sst / ion

SST v3
https://sst.dev
MIT License
2.13k stars 249 forks source link

It's hard to extend the fallback viewer request Cloudfront function #663

Open birtles opened 3 months ago

birtles commented 3 months ago

As mentioned on Discord, in my Astro site I'd like to run a Cloudfront function to perform redirects based on the user's Accept-Language header. However, It looks like by the time the transform for the distribution runs the "injections" for that function have already been merged into a CF function.

As I understand it, my options are (1) fork all the code in astro.ts and add the necessary logic, (2) introduce an extra behavior that runs before the fallback one and do the redirect there (assuming that's possible).

I'm not sure what the solution is. I'm new to SST but it appears that v2 might have passed the "plan" to the transform function so it was easier to add further injections?

Non-solutions:

fwang commented 3 months ago

Appreciate the details @birtles! Do you just need to inject into the CloudFront function for requests routed to Lambda origins? Or also for requests routed to s3 origins?

birtles commented 2 months ago

Hi @fwang, sorry I'm still new to SST so I please correct me if I get things mixed up.

As I understand it SST will generate two origins and one origin group:

Then there are three behaviors:

If that summary is right, then I think the answer to your question is I want to inject into the CF function to redirect to S3 origins.

Basically Astro will generate:

client/
├─ en/
│  ├─ index.html
│  ├─ some-path/
│  │  ├─ index.html
├─ ja/
│  ├─ index.html
│  ├─ some-path/
│  │  ├─ index.html

If the incoming request is for /some-path then I want to return a 302 redirect to /en/some-path or /ja/some-path based on the Accept-Language.

(In future, I'd also like to internally redirect some incoming requests based on UA string when we have different resources optimized for different browsers but I might be able to set up specific behaviors for those routes without touching the viewer request function on the default behavior, I'm not sure.)

birtles commented 2 months ago

I made a start on trying to address this with the current API. I'm completely new to SST and Pulumi but as far as I can tell, it turns out to be a bit of work just to get to the starting line.

  1. Install @pulumi/pulumi and @pulumi/aws as dev dependencies.
  2. Create an sst.config.ts something like the following:
/// <reference path="./.sst/platform/config.d.ts" />

import { cloudfront } from '@pulumi/aws';
import { all, type Input } from '@pulumi/pulumi';
import type { BuildMetaConfig } from 'astro-sst/build-meta';
import * as fs from 'node:fs';
import * as path from 'node:path';

export default $config({
  app(input) {
    return {
      name: 'my-app',
      removal: input?.stage === 'production' ? 'retain' : 'remove',
      home: 'aws',
    };
  },
  async run() {
    const astroSite = new sst.aws.Astro('AstroSite', {
      transform: {
        cdn: (cdn) => {
          const viewerRequest = new cloudfront.Function(
            // TODO: Generate a more suitable name
            'AstroSiteTestViewerRequest',
            {
              runtime: 'cloudfront-js-1.0',
              code: getViewerRequestCode(),
            },
            { parent: astroSite }
          );

          cdn.defaultCacheBehavior = all([
            cdn.defaultCacheBehavior,
            viewerRequest.arn,
          ]).apply(([behavior, viewerRequestArn]) => {
            // TODO: Don't clobber other functions
            behavior.functionAssociations = [
              {
                eventType: 'viewer-request',
                functionArn: viewerRequestArn,
              },
            ];

            return behavior;
          });
        },
      },
    });
  },
});

function getViewerRequestCode(): Input<string> {
  // TODO: Handle the site path correctly
  const filePath = path.join('.', 'dist', 'sst.buildMeta.json');
  if (!fs.existsSync(filePath)) {
    throw new Error(`Could not find build meta file at ${filePath}.`);
  }
  const buildMeta = JSON.parse(
    fs.readFileSync(filePath, 'utf-8')
  ) as BuildMetaConfig;

  // XXX Fork all the handling of the buildMeta.routes here
  // (and the forwarded host header handling etc.)

  return Promise.resolve(`function handler(event) {
var request = event.request;
// Viewer request logic here
return request;
}`);
}

After doing that you still need to fork a bunch of code from astro.ts and elsewhere and hope it doesn't get any important updates/fixes in the future.

It's also going to break if, for example, the build meta filename or format changes. And then there are all the TODOs too.

fwang commented 2 months ago

@birtles is the locale redirect something that can be baked into the Astro component, or is it something custom to ur usecase?

Can u share the code that need to be injected into the CF function?

birtles commented 2 months ago

@birtles is the locale redirect something that can be baked into the Astro component, or is it something custom to ur usecase?

@fwang I'm afraid I don't think you can bake it into the Astro component (see my first message). Basically, there are all sorts of different behaviors you might want:

And then there are plenty of other cases for wanting to customize the viewer request function like UA-string based redirects.