Created
April 8, 2023 02:06
-
-
Save tappleby/6ed416fad9b2b49ea402abf133e1cabc to your computer and use it in GitHub Desktop.
Sample remix handler based on remix-architect that supports AWS Lambda Streaming - https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createRequestHandler } from "./streaming-lambda.ts"; | |
import * as build from "@remix-run/dev/server-build"; | |
export const handler = awslambda.streamifyResponse( | |
createRequestHandler({ | |
build, | |
mode: process.env.NODE_ENV, | |
}) | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { | |
AppLoadContext, | |
ServerBuild, | |
RequestInit as NodeRequestInit, | |
Response as NodeResponse, | |
} from "@remix-run/node"; | |
import { | |
AbortController as NodeAbortController, | |
createRequestHandler as createRemixRequestHandler, | |
Headers as NodeHeaders, | |
Request as NodeRequest, | |
writeReadableStreamToWritable, | |
} from "@remix-run/node"; | |
import type { | |
APIGatewayProxyEventHeaders, | |
APIGatewayProxyEventV2, | |
APIGatewayProxyStructuredResultV2, | |
Context, | |
} from "aws-lambda"; | |
import type { Writable } from "stream"; | |
/** | |
* A function that returns the value to use as `context` in route `loader` and | |
* `action` functions. | |
* | |
* You can think of this as an escape hatch that allows you to pass | |
* environment/platform-specific values through to your loader/action. | |
*/ | |
export type GetLoadContextFunction = ( | |
event: APIGatewayProxyEventV2 | |
) => AppLoadContext; | |
export type StreamingRequestHandler = ( | |
event: APIGatewayProxyEventV2, | |
responseStream: Writable, | |
context: Context | |
) => void; | |
/** | |
* Returns a request handler for Architect that serves the response using | |
* Remix. | |
*/ | |
export function createRequestHandler({ | |
build, | |
getLoadContext, | |
mode = process.env.NODE_ENV, | |
}: { | |
build: ServerBuild; | |
getLoadContext?: GetLoadContextFunction; | |
mode?: string; | |
}): StreamingRequestHandler { | |
let handleRequest = createRemixRequestHandler(build, mode); | |
return async (event, responseStream, _context) => { | |
let request = createRemixRequest(event, responseStream); | |
let loadContext = getLoadContext?.(event); | |
let response = (await handleRequest(request, loadContext)) as NodeResponse; | |
return sendRemixResponse(response, responseStream); | |
}; | |
} | |
export function createRemixRequest( | |
event: APIGatewayProxyEventV2, | |
res: Writable | |
): NodeRequest { | |
let host = event.headers["x-forwarded-host"] || event.headers.host; | |
let search = event.rawQueryString.length ? `?${event.rawQueryString}` : ""; | |
let scheme = process.env.ARC_SANDBOX ? "http" : "https"; | |
let url = new URL(`${scheme}://${host}${event.rawPath}${search}`); | |
let isFormData = event.headers["content-type"]?.includes( | |
"multipart/form-data" | |
); | |
let controller = new NodeAbortController(); | |
res.on("close", () => controller.abort()); | |
return new NodeRequest(url.href, { | |
method: event.requestContext.http.method, | |
headers: createRemixHeaders(event.headers, event.cookies), | |
// Cast until reason/throwIfAborted added | |
// https://github.com/mysticatea/abort-controller/issues/36 | |
signal: controller.signal as NodeRequestInit["signal"], | |
body: | |
event.body && event.isBase64Encoded | |
? isFormData | |
? Buffer.from(event.body, "base64") | |
: Buffer.from(event.body, "base64").toString() | |
: event.body, | |
}); | |
} | |
export function createRemixHeaders( | |
requestHeaders: APIGatewayProxyEventHeaders, | |
requestCookies?: string[] | |
): NodeHeaders { | |
let headers = new NodeHeaders(); | |
for (let [header, value] of Object.entries(requestHeaders)) { | |
if (value) { | |
headers.append(header, value); | |
} | |
} | |
if (requestCookies) { | |
headers.append("Cookie", requestCookies.join("; ")); | |
} | |
return headers; | |
} | |
export async function sendRemixResponse( | |
nodeResponse: NodeResponse, | |
res: Writable | |
): Promise<void> { | |
let cookies: string[] = []; | |
// Arc/AWS API Gateway will send back set-cookies outside of response headers. | |
for (let [key, values] of Object.entries(nodeResponse.headers.raw())) { | |
if (key.toLowerCase() === "set-cookie") { | |
for (let value of values) { | |
cookies.push(value); | |
} | |
} | |
} | |
if (cookies.length) { | |
nodeResponse.headers.delete("Set-Cookie"); | |
} | |
let metadata: APIGatewayProxyStructuredResultV2 = { | |
statusCode: nodeResponse.status, | |
headers: Object.fromEntries(nodeResponse.headers.entries()), | |
cookies, | |
}; | |
let httpResponseStream = awslambda.HttpResponseStream.from( | |
res, | |
metadata | |
) as Writable; | |
if (nodeResponse.body) { | |
await writeReadableStreamToWritable(nodeResponse.body, httpResponseStream); | |
} else { | |
res.end(); | |
} | |
} |
@owlyowl I havent tried it using api gateway but I dont believe it works with streaming. With the POC I had Cloudfront -> Streaming Lambda directly - https://github.com/tappleby/remix-aws-streaming-example/blob/5c4fe9ac98cde8373ce61dad92a05c4f279c5593/cdk/lib/remix-streaming-stack.ts#L56-L100
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Did you get a chance to work out if this works as I thought a limitation of streaming lambdas was they couldn't be behind apigateway, they had to be directly invoked?