solidjs / solid-start

SolidStart, the Solid app framework
https://start.solidjs.com
MIT License
4.93k stars 372 forks source link

[Bug?]: SolidStart aws_lambda preset unable to serve JS and CSS artifacts when deployed to AWS Lambda #1524

Closed gotgenes closed 5 days ago

gotgenes commented 3 weeks ago

Duplicates

Latest version

Current behavior 😯

The handler in .output/server/index.js produced when using

npm run build -- --preset aws_lambda

from a new SolidStart project is able to handle the initial request and provide the initial rendered HTML, but fails to return the remaining requested assets (JavaScript chunks and CSS). Responses coming back for the remaining requested assets are all 404 page HTML renderings, instead of the expected filetype (JavaScript or CSS). The JavaScript files all have responses like this:

<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="icon" href="/favicon.ico"><script>window._$HY||(e=>{let t=e=>e&&e.hasAttribute&&(e.hasAttribute("data-hk")?e:t(e.host&&e.host.nodeType?e.host:e.parentNode));["click", "input"].forEach((o=>document.addEventListener(o,(o=>{let a=o.composedPath&&o.composedPath()[0]||o.target,s=t(a);s&&!e.completed.has(s)&&e.events.push([s,o])}))))})(_$HY={events:[],completed:new WeakSet,r:{},fe(){}});</script><script>self.$R=self.$R||[];_$HY.r["0-0-0-0-0-0-0-1-0-0-0-1-0"]=$R[0]=($R[1]=(s,f,p)=>((p=new Promise((a,b)=>{s=a,f=b})).s=s,p.f=f,p))();($R[2]=(p,d)=>{p.s(d),p.status="success",p.value=d;delete p.s;delete p.f})($R[0],!0);</script><!--xs--><link href="/_build/assets/client-Hm9xi2M4.css" rel="stylesheet" precendence="high" /><link href="/_build/assets/routing-DXBUw-N3.js" rel="modulepreload" /><link href="/_build/assets/client-BXWCUTgz.js" rel="modulepreload" /><link href="/_build/assets/components-Dro7JrPp.js" rel="modulepreload" /><link href="/_build/assets/_...404_-DUYVcV2b.js" rel="modulepreload" /></head><body><div id="app"><!--!$e0-0-0-0-0-0--><nav data-hk="0-0-0-0-0-0-0-1-0-0-0-0-0" class="bg-sky-800"><ul class="container flex items-center p-3 text-gray-200"><li class="border-b-2 border-transparent hover:border-sky-600 mx-1.5 sm:mx-6"><a href="/">Home</a></li><li class="border-b-2 border-transparent hover:border-sky-600 mx-1.5 sm:mx-6"><a href="/about">About</a></li></ul></nav><main data-hk="0-0-0-0-0-0-0-1-0-0-0-1-0-0-0-0-0-0-0" class="text-center mx-auto text-gray-700 p-4"><h1 class="max-6-xs text-6xl text-sky-700 font-thin uppercase my-16">Not Found</h1><p class="mt-8">Visit <a href="https://solidjs.com" target="_blank" class="text-sky-600 hover:underline">solidjs.com</a> to learn how to build Solid apps.</p><p class="my-4"><!--$--><a data-hk="0-0-0-0-0-0-0-1-0-0-0-1-0-0-0-0-0-0-1-0" href="/" class="text-sky-600 hover:underline active" link="true" >Home</a><!--/--> - <!--$--><a data-hk="0-0-0-0-0-0-0-1-0-0-0-1-0-0-0-0-0-0-2-0" href="/about" class="text-sky-600 hover:underline inactive" link="true" >About Page</a><!--/--></p></main><!--!$/e0-0-0-0-0-0--></div><!--$--><script>window.manifest = {"src/routes/[...404].tsx?pick=default&pick=$css":{"output":"/_build/assets/_...404_-DUYVcV2b.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-DXBUw-N3.js","key":"/_build/assets/routing-DXBUw-N3.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-Dro7JrPp.js","key":"/_build/assets/components-Dro7JrPp.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/_...404_-DUYVcV2b.js","key":"/_build/assets/_...404_-DUYVcV2b.js","rel":"modulepreload"}}]},"src/routes/about.tsx?pick=default&pick=$css":{"output":"/_build/assets/about-6s4VW_6B.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-DXBUw-N3.js","key":"/_build/assets/routing-DXBUw-N3.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/Counter-B3N4pNo9.js","key":"/_build/assets/Counter-B3N4pNo9.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-Dro7JrPp.js","key":"/_build/assets/components-Dro7JrPp.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/about-6s4VW_6B.js","key":"/_build/assets/about-6s4VW_6B.js","rel":"modulepreload"}}]},"src/routes/index.tsx?pick=default&pick=$css":{"output":"/_build/assets/index-BC7X7foe.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-DXBUw-N3.js","key":"/_build/assets/routing-DXBUw-N3.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/Counter-B3N4pNo9.js","key":"/_build/assets/Counter-B3N4pNo9.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-Dro7JrPp.js","key":"/_build/assets/components-Dro7JrPp.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/index-BC7X7foe.js","key":"/_build/assets/index-BC7X7foe.js","rel":"modulepreload"}}]},"virtual:#vinxi/handler/client":{"output":"/_build/assets/client-BXWCUTgz.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/client-Hm9xi2M4.css","key":"/_build/assets/client-Hm9xi2M4.css","rel":"stylesheet","precendence":"high"}},{"tag":"link","attrs":{"href":"/_build/assets/routing-DXBUw-N3.js","key":"/_build/assets/routing-DXBUw-N3.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/client-BXWCUTgz.js","key":"/_build/assets/client-BXWCUTgz.js","rel":"modulepreload"}}]}}</script><script type="module" async src="/_build/assets/client-BXWCUTgz.js"></script><!--/--></body></html>

The status code for these responses is 200, not 404.

Expected behavior 🤔

The bundle produced serves an identical rendered page to the local rendering seen when using npm run dev. The CSS contents are CSS, the JavaScript contents are JavaScript, and the status codes for the responses are 200.

If the 404 page is rendered, I'd like to see a 404 status code in the response, not a 200.

Steps to reproduce 🕹

Steps:

  1. Create a new SolidStart project:

    npm init solid@latest

    I chose "with-tailwindcss" but I don't think it matters.

  2. Build the SolidStart bundle targeting AWS Lambda deployment:

    npm run build -- --preset aws_lambda
  3. Install the CDK dependencies:

    npm install --save-dev aws-cdk@latest
    npm install aws-cdk-lib@latest
  4. Create the CDK directories:

    mkdir -p cdk/lib
    mkdir -p cdk/bin
  5. Add the CDK stack to cdk/lib/solid-stack.ts:

    import * as cdk from "aws-cdk-lib";
    import { Construct } from "constructs";
    import * as apigw from "aws-cdk-lib/aws-apigatewayv2";
    import * as lambda from "aws-cdk-lib/aws-lambda";
    import * as integrations from "aws-cdk-lib/aws-apigatewayv2-integrations";
    
    export class SolidStartStack extends cdk.Stack {
     constructor(scope: Construct, id: string, props?: cdk.StackProps) {
       super(scope, id, props);
    
       const backendLambda = new lambda.Function(this, "SolidBackendLambda", {
         runtime: lambda.Runtime.NODEJS_20_X,
         handler: "index.handler",
         code: lambda.Code.fromAsset("./.output/server"),
         architecture: lambda.Architecture.ARM_64,
       });
    
       const api = new apigw.HttpApi(this, "Endpoint", {
         defaultIntegration: new integrations.HttpLambdaIntegration(
           "SolidIntegration",
           backendLambda,
         ),
       });
    
       new cdk.CfnOutput(this, "HTTP API URL", {
         value: api.url ?? "Something went wrong with the deploy",
       });
     }
    }
  6. Add the CDK app to cdk/bin/cdk.ts:

    #!/usr/bin/env node
    import "source-map-support/register";
    import * as cdk from "aws-cdk-lib";
    import { SolidStartStack } from "../lib/solid-stack";
    
    const app = new cdk.App();
    new SolidStartStack(app, "SolidStartApp", {
     /* If you don't specify 'env', this stack will be environment-agnostic.
      * Account/Region-dependent features and context lookups will not work,
      * but a single synthesized template can be deployed anywhere. */
    
     /* Uncomment the next line to specialize this stack for the AWS Account
      * and Region that are implied by the current CLI configuration. */
     env: {
       account: process.env.CDK_DEFAULT_ACCOUNT,
       region: process.env.CDK_DEFAULT_REGION,
     },
    
     /* Uncomment the next line if you know exactly what Account and Region you
      * want to deploy the stack to. */
     // env: { account: '123456789012', region: 'us-east-1' },
    
     /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
    });
  7. Add the cdk.json file:

    {
     "app": "npx tsx cdk/bin/cdk.ts",
     "watch": {
       "include": ["cdk/**"],
       "exclude": [
         "README.md",
         "cdk*.json",
         "**/*.d.ts",
         "**/*.js",
         "tsconfig.json",
         "package*.json",
         "yarn.lock",
         "node_modules",
         "test"
       ]
     },
     "context": {
       "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
       "@aws-cdk/core:checkSecretUsage": true,
       "@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
       "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
       "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
       "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
       "@aws-cdk/aws-iam:minimizePolicies": true,
       "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
       "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
       "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
       "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
       "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
       "@aws-cdk/core:enablePartitionLiterals": true,
       "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
       "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
       "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
       "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
       "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
       "@aws-cdk/aws-route53-patters:useCertificate": true,
       "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
       "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
       "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
       "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
       "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
       "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
       "@aws-cdk/aws-redshift:columnId": true,
       "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
       "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
       "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
       "@aws-cdk/aws-kms:aliasNameRef": true,
       "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
       "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
       "@aws-cdk/aws-efs:denyAnonymousAccess": true,
       "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
       "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
       "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
       "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
       "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
       "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
       "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
       "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
       "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
       "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
       "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
       "@aws-cdk/aws-eks:nodegroupNameAttribute": true,
       "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true
     }
    }
  8. Export your CDK environment variables:

    export CDK_DEFAULT_ACCOUNT=123456789012
    export CDK_DEFAULT_REGION=us-east-1
  9. Ensure AWS CLI credentials are set up; for me this looks like:

    aws sso login
  10. Deploy the CDK stack:

    npx cdk deploy
  11. The deployment should succeed, and you should see an output like:

    Outputs:
    SolidStartApp.HTTPAPIURL = https://258e18eyl0.execute-api.us-east-1.amazonaws.com/
    Stack ARN:
    arn:aws:cloudformation:us-east-1:123456789012:stack/SolidStartApp/9cd014e0-2208-11ef-be86-0222a0ec1d5f

    where the HTTPAPIURL should be a valid URL that you can hit to see your SolidStart app.

  12. Open the URL in your browser and you should see a basic HTML page with the counter button and with no styling. Inspecting the network calls, you should see that the responses for the CSS and JS files are HTML with the rendered 404 page, but with status code 200.

Context 🔦

I'm trying to deploy a SSR SolidStart site with AWS Lambda as the deployment target. I expect the Lambda to serve the dynamic content as well as the static assets by default.

Later I would like to gather the static client assets and deploy them to CloudFront, letting CloudFront serve the static content and the Lambda instance serving the dynamic portion, but right now, I'm just trying to prove out I can get a deployment target of AWS Lambda to work.

I was attracted to SolidStart as a framework because, after a bit of digging, I realized that users seemed to be able to leverage all the deployment providers available in Nitro.

It's possible this functionality is entirely Vinxi's responsibility and it never worked for any meta-metaframework built on it—not only SolidStart—however, I'm starting here with SolidStart because that's what I'm intending to use, and I'm not yet familiar with this Jenga tower of SolidStart→Vinxi→Nitro→Vite→Rollup (and anything else I missed). 😅

Your environment 🌎

No response

Brendonovich commented 2 weeks ago

Reproduction repo here.

Nitro's aws-lambda preset doesn't serve static assets by default (serveStatic isn't true)

image

To override this you can set serveStatic: true yourself

import { defineConfig } from "@solidjs/start/config";

export default defineConfig({
  server: {
    preset: "aws-lambda",
    serveStatic: true,
  },
});

And change your CDK config to include all of .output

const backendLambda = new lambda.Function(this, "SolidBackendLambda", {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: "server/index.handler",
  code: lambda.Code.fromAsset("./.output"),
  architecture: lambda.Architecture.ARM_64,
});

Though seeing as each static asset would require a function invocation, it might be better to put the assets in CloudFront for faster access. SST Ion's Solid Start component does this via Pulumi.

ryansolid commented 1 week ago

@Brendonovich what's your recommendation here? It sounds like this might be by design? It doesn't sound like Start could do much here. Is there something that could be submitted against Nitro?

Brendonovich commented 6 days ago

Yeah I'd say this is by design. If Nitro were to do anything i think they should mention in the lambda preset docs that the files in .output/public need to be hosted somewhere else under the same domain, usually CloudFront. I don't think there's anything Start can/should do here.

gotgenes commented 5 days ago

Thank you, @Brendonovich, for the helpful reply. I am in agreement. I was interested in a single-service (i.e., Lambda-only) deployment of the build from the perspective of simplicity, but it's not a good setup for production. It is an anti-pattern given that Lambda is pay-by-use and you're wasting CPU time funneling static assets through it.

It turns out not much more difficult to go ahead and set up CloudFront with the server-bundle Lambda as the default origin, and the static frontend bundle/assets from an S3 origin. I think that is the sane default, to push users into "the pit of success".