feat: Add Scaleway serverless function preset#3970
feat: Add Scaleway serverless function preset#3970Siilwyn wants to merge 3 commits intonitrojs:mainfrom
Conversation
|
@Siilwyn is attempting to deploy a commit to the Nitro Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a new Scaleway Serverless preset: introduces preset module and runtime handler, updates preset registry and types, adds Scaleway deployment docs, and includes an integration test for the handler. (49 words) Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
dfc067b to
1b2a136
Compare
|
Moved from draft so CodeRabbit picks it up. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/presets/scaleway/runtime/scaleway-serverless.ts`:
- Around line 11-29: In handler, remove non-null assertions on event.headers and
event.queryStringParameters and guard them with safe fallbacks (e.g., const hdrs
= event.headers ?? {}; const qs = event.queryStringParameters ?? {}), iterate
hdrs safely when building headers map, and perform a case-insensitive lookup for
X-Forwarded-Proto (check hdrs["x-forwarded-proto"] and hdrs["X-Forwarded-Proto"]
or normalize keys) when composing the origin for joinURL; also handle body
decoding based on event.isBase64Encoded (decode to string/buffer when true) and
treat both GET and HEAD as having no body when constructing the Request. Ensure
you update references in handler to use these new locals (hdrs, qs, decodedBody)
and remove any remaining ! non-null assertions.
🧹 Nitpick comments (1)
docs/2.deploy/20.providers/scaleway.md (1)
1-7: Add a minimal config snippet for quicker onboarding.A short
nitro.configexample would make the preset immediately actionable for readers skimming the page.✍️ Example addition
# Scaleway > Deploy Nitro apps to Scaleway. **Preset:** `scaleway-serverless` +```ts +// nitro.config.ts +export default defineNitroConfig({ + preset: "scaleway-serverless", +}); +``` + :read-more{title="Scaleway Serverless Functions" to="https://www.scaleway.com/en/docs/serverless-functions/"}
| export async function handler(event: Event, context: Context) { | ||
| const headers = Object.fromEntries( | ||
| Object.entries(event.headers!).map(([key, value]) => [key, String(value)]) | ||
| ); | ||
|
|
||
| const url = withQuery( | ||
| joinURL( | ||
| headers?.["X-Forwarded-Proto"] === "http" ? "http://" : "https://", | ||
| headers.host, | ||
| event.path | ||
| ), | ||
| event.queryStringParameters! | ||
| ); | ||
|
|
||
| const request = new Request(url, { | ||
| method: event.httpMethod, | ||
| headers, | ||
| body: event.httpMethod === "GET" ? undefined : event.body, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the Scaleway adapter structure
find . -type f -name "*scaleway*" | head -20Repository: nitrojs/nitro
Length of output: 183
🏁 Script executed:
# Check the full Scaleway serverless.ts file for context
cat -n src/presets/scaleway/runtime/scaleway-serverless.tsRepository: nitrojs/nitro
Length of output: 1191
🏁 Script executed:
# Look for similar event handlers in other presets for comparison
find src/presets -name "*.ts" -type f | xargs grep -l "handler.*event.*context" | head -10Repository: nitrojs/nitro
Length of output: 108
🌐 Web query:
Scaleway serverless functions Node.js event schema headers queryStringParameters isBase64Encoded
💡 Result:
In Scaleway Serverless Functions (Node.js), the HTTP-triggered handler receives an event object with (at least) these fields: path (string), queryStringParameters (object/map), body (string), headers (object/map), httpMethod (string), and isBase64Encoded (boolean). [1]
A common (expanded) schema used by Scaleway also mirrors an API-Gateway-proxy style request and may include: resource, path, httpMethod, headers, multiValueHeaders, queryStringParameters, multiValueQueryStringParameters, pathParameters, stageVariables, requestContext, body, isBase64Encoded. [2]
Example skeleton:
module.exports.myHandler = async (event) => {
// event.headers
// event.queryStringParameters
// event.isBase64Encoded
// event.body
};[1][2]
Sources: [1] Scaleway Docs – “Code examples for Serverless Functions” (event components for Node.js) (scaleway.com), [2] github.com/scaleway/scaleway-functions-go/events – APIGatewayProxyRequest fields (pkg.go.dev)
Citations:
- 1: https://www.scaleway.com/en/docs/serverless-functions/reference-content/code-examples/
- 2: https://pkg.go.dev/github.com/scaleway/scaleway-functions-go/events
🏁 Script executed:
# Check if `@scaleway/serverless-functions` is in package.json or node_modules
grep -r "scaleway/serverless-functions" . --include="*.json" --include="*.ts" | head -10Repository: nitrojs/nitro
Length of output: 273
🏁 Script executed:
# Look at the test file for Scaleway adapter
cat -n test/presets/scaleway.test.tsRepository: nitrojs/nitro
Length of output: 1085
🏁 Script executed:
# Look at other presets to see how they handle similar situations (AWS Lambda, Google Cloud, etc.)
find src/presets -name "*entry*.ts" -type f | head -10Repository: nitrojs/nitro
Length of output: 96
🏁 Script executed:
# Check how other presets handle base64 bodies and optional headers
grep -r "isBase64Encoded\|event.headers\|event.body" src/presets --include="*.ts" -B 2 -A 2 | head -60Repository: nitrojs/nitro
Length of output: 3702
🏁 Script executed:
# Check for header case normalization patterns in the codebase
grep -r "toLowerCase\|headers\[" src/ --include="*.ts" | grep -i header | head -20Repository: nitrojs/nitro
Length of output: 1255
🏁 Script executed:
# Check how Stormkit handles base64 to see if there's a shared utility
cat -n src/presets/stormkit/runtime/stormkit.tsRepository: nitrojs/nitro
Length of output: 2126
🏁 Script executed:
# Check if there's a shared utility function for base64 bodies
grep -r "awsResponseBody\|isBase64Encoded" src/ --include="*.ts" | grep -v "node_modules"Repository: nitrojs/nitro
Length of output: 940
Harden event normalization to prevent crashes and ensure case-insensitive header handling.
The non-null assertions on event.headers! and event.queryStringParameters! can cause runtime crashes if these fields are undefined. Additionally, header lookups should be case-insensitive (check both cases for X-Forwarded-Proto), and the isBase64Encoded flag must be handled for request bodies. The adapter should also handle HEAD requests without a body.
The AWS Lambda preset in this repository shows the established pattern for these concerns:
- Safe fallback for optional fields instead of non-null assertions
- Case-insensitive header lookups for protocol detection
- Base64 decoding when
isBase64Encodedis true - Safe iteration and value checks
🛠️ Proposed hardening
export async function handler(event: Event, context: Context) {
- const headers = Object.fromEntries(
- Object.entries(event.headers!).map(([key, value]) => [key, String(value)])
- );
+ const rawHeaders = event.headers ?? {};
+ const headers = Object.fromEntries(
+ Object.entries(rawHeaders).map(([key, value]) => [key, String(value)])
+ );
+ const proto =
+ (headers["x-forwarded-proto"] ?? headers["X-Forwarded-Proto"] ?? "https").toLowerCase();
+ const host = headers.host ?? headers.Host;
+ if (!host) {
+ throw new Error("Missing Host header in Scaleway event");
+ }
const url = withQuery(
joinURL(
- headers?.["X-Forwarded-Proto"] === "http" ? "http://" : "https://",
- headers.host,
- event.path
+ proto === "http" ? "http://" : "https://",
+ host,
+ event.path
),
- event.queryStringParameters!
+ event.queryStringParameters ?? {}
);
const request = new Request(url, {
method: event.httpMethod,
headers,
- body: event.httpMethod === "GET" ? undefined : event.body,
+ body:
+ event.httpMethod === "GET" || event.httpMethod === "HEAD"
+ ? undefined
+ : event.isBase64Encoded
+ ? Buffer.from(event.body || "", "base64")
+ : event.body,
});
return nitroApp.fetch(request);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function handler(event: Event, context: Context) { | |
| const headers = Object.fromEntries( | |
| Object.entries(event.headers!).map(([key, value]) => [key, String(value)]) | |
| ); | |
| const url = withQuery( | |
| joinURL( | |
| headers?.["X-Forwarded-Proto"] === "http" ? "http://" : "https://", | |
| headers.host, | |
| event.path | |
| ), | |
| event.queryStringParameters! | |
| ); | |
| const request = new Request(url, { | |
| method: event.httpMethod, | |
| headers, | |
| body: event.httpMethod === "GET" ? undefined : event.body, | |
| }); | |
| export async function handler(event: Event, context: Context) { | |
| const rawHeaders = event.headers ?? {}; | |
| const headers = Object.fromEntries( | |
| Object.entries(rawHeaders).map(([key, value]) => [key, String(value)]) | |
| ); | |
| const proto = | |
| (headers["x-forwarded-proto"] ?? headers["X-Forwarded-Proto"] ?? "https").toLowerCase(); | |
| const host = headers.host ?? headers.Host; | |
| if (!host) { | |
| throw new Error("Missing Host header in Scaleway event"); | |
| } | |
| const url = withQuery( | |
| joinURL( | |
| proto === "http" ? "http://" : "https://", | |
| host, | |
| event.path | |
| ), | |
| event.queryStringParameters ?? {} | |
| ); | |
| const request = new Request(url, { | |
| method: event.httpMethod, | |
| headers, | |
| body: | |
| event.httpMethod === "GET" || event.httpMethod === "HEAD" | |
| ? undefined | |
| : event.isBase64Encoded | |
| ? Buffer.from(event.body || "", "base64") | |
| : event.body, | |
| }); | |
| return nitroApp.fetch(request); | |
| } |
🤖 Prompt for AI Agents
In `@src/presets/scaleway/runtime/scaleway-serverless.ts` around lines 11 - 29, In
handler, remove non-null assertions on event.headers and
event.queryStringParameters and guard them with safe fallbacks (e.g., const hdrs
= event.headers ?? {}; const qs = event.queryStringParameters ?? {}), iterate
hdrs safely when building headers map, and perform a case-insensitive lookup for
X-Forwarded-Proto (check hdrs["x-forwarded-proto"] and hdrs["X-Forwarded-Proto"]
or normalize keys) when composing the origin for joinURL; also handle body
decoding based on event.isBase64Encoded (decode to string/buffer when true) and
treat both GET and HEAD as having no body when constructing the Request. Ensure
you update references in handler to use these new locals (hdrs, qs, decodedBody)
and remove any remaining ! non-null assertions.
There was a problem hiding this comment.
Since all tests pass I don't think this is needed?
event.queryStringParameterscan benullbecausewithQueryhandles this.- The expected headers are set by the Scaleway runtime afaik, so
X-Forwarded-Protoshould always be in this casing. - Maybe there are no tests for
isBase64Encoded, is this expected to work? - Are
HEADrequests without a body also not tested?
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the implementation of withQuery to see how it handles null/undefined
cat -n node_modules/ufo/dist/index.mjs | grep -A 30 "withQuery"
Length of output: 63
🏁 Script executed:
#!/bin/bash
# Look at how other serverless presets handle headers and query parameters, especially AWS Lambda
cat -n src/presets/aws-lambda/runtime/lambda-server.ts
Length of output: 79
🏁 Script executed:
#!/bin/bash
# Check if there are tests for base64 encoded bodies in the test file
cat -n test/presets/scaleway.test.ts
Length of output: 1028
🏁 Script executed:
#!/bin/bash
# Check the `@scaleway/serverless-functions` types to see if isBase64Encoded is part of the event
fd -e ts -e d.ts . | xargs grep -l "scaleway.*serverless" | head -5
Length of output: 138
🏁 Script executed:
#!/bin/bash
# Find the ufo package and check withQuery implementation
npm list ufo 2>/dev/null || echo "checking package.json for ufo"
cat package.json | jq '.dependencies.ufo // .devDependencies.ufo // empty'
Length of output: 90
🏁 Script executed:
#!/bin/bash
# Search for AWS Lambda runtime files in presets
fd -t f "lambda" src/presets/ | head -10
Length of output: 99
🏁 Script executed:
#!/bin/bash
# Look at the types being used - check the serveHandler type definition
rg -A 20 "type Event = " src/presets/scaleway/runtime/scaleway-serverless.ts
Length of output: 655
🏁 Script executed:
#!/bin/bash
# Check what fields are documented in web searches or comments about Scaleway events
rg -i "isBase64Encoded" src/presets/ -C 3
Length of output: 2358
🏁 Script executed:
#!/bin/bash
# Look at other presets that might handle query parameters to see the pattern
rg "withQuery" src/presets/ -C 3
Length of output: 1114
🏁 Script executed:
#!/bin/bash
# Check AWS Lambda runtime to see how they handle headers and query params
cat -n src/presets/aws-lambda/runtime/aws-lambda.ts
Length of output: 929
🏁 Script executed:
#!/bin/bash
# Look at the _utils.ts that has the request body handling
cat -n src/presets/aws-lambda/runtime/_utils.ts
Length of output: 5523
🏁 Script executed:
#!/bin/bash
# Check the ufo source or types to understand withQuery behavior
npm info ufo | grep -A 5 "version\|description"
Length of output: 162
🌐 Web query:
ufo withQuery function null undefined behavior
💡 Result:
In UnJS ufo, withQuery(input, query) expects query to be an object. Internally it does: { ...parseQuery(existing), ...query }, so:
withQuery("/x", null)andwithQuery("/x", undefined)throw (because spreadingnull/undefinedinto an object is aTypeError). [1]- For individual query values when
queryis an object:undefinedvalues are omitted entirely (key removed). [1]null(and"") become a key without=(e.g.{ a: null }⇒?a). [1]0/falseare stringified and included normally (?a=0,?a=false). [1]
If you want “no change / no extra params”, pass {} instead of null/undefined. If you want to remove a param, set it to undefined. [1]
🌐 Web query:
Scaleway serverless functions event schema isBase64Encoded field
💡 Result:
In Scaleway Serverless Functions, the HTTP-trigger event includes an isBase64Encoded boolean flag alongside body, headers, etc. It means “whether the request body is base64 encoded.” If it’s true, you should base64-decode event.body before using it; if it’s false, you can treat event.body as the plain body string. [1]
Example (Node.js):
const rawBody = event.isBase64Encoded
? Buffer.from(event.body, "base64")
: Buffer.from(event.body ?? "", "utf8");Example (Python):
import base64
raw = base64.b64decode(event["body"]) if event["isBase64Encoded"] else event["body"].encode()Sources: [1] https://www.scaleway.com/en/docs/serverless-functions/reference-content/code-examples/
@Siilwyn, thank you for the feedback! While the tests are passing, there are a few edge cases worth addressing:
1. withQuery and null handling:
According to the ufo documentation, withQuery(input, null) and withQuery(input, undefined) actually throw a TypeError because they try to spread null/undefined into an object. So if event.queryStringParameters is null, the current code would crash. Using event.queryStringParameters ?? {} is the safe approach.
2. Header casing:
You're right that Scaleway likely provides X-Forwarded-Proto in a consistent casing. However, looking at how the AWS Lambda preset handles this (lines 52-53 in src/presets/aws-lambda/runtime/_utils.ts), it checks both cases as a defensive practice:
event.headers["X-Forwarded-Proto"] || event.headers["x-forwarded-proto"]This pattern protects against runtime variations or middleware that might normalize headers. It's a small safety check that costs nothing.
3. isBase64Encoded support:
According to Scaleway's documentation, the event schema does include isBase64Encoded. The current code doesn't handle it, which means binary uploads (images, PDFs, etc.) would be corrupted. The AWS Lambda preset shows the proper pattern (lines 97-100):
if (event.isBase64Encoded) {
return Buffer.from(event.body || "", "base64");
}
return event.body;Would you like to add test coverage for base64-encoded request bodies to ensure this works correctly? This would catch the issue before it affects production workloads.
🧠 Learnings used
Learnt from: medz
Repo: nitrojs/nitro PR: 3834
File: src/presets/cloudflare/server-entry.ts:63-63
Timestamp: 2025-12-03T19:09:13.905Z
Learning: In the Nitro Cloudflare preset (src/presets/cloudflare/server-entry.ts), native errors from oxc-parser and Node.js readFile operations should be allowed to bubble up naturally without wrapping, as their native error messages are more user-friendly and provide better diagnostic information.
There was a problem hiding this comment.
Fixed the query handling and header casing.
The base 64 encoding is indeed in the type and documentation but I don't see a way to configure this.
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Scaleway serverless functions binary request body base64 configuration
💡 Result:
Scaleway Serverless Functions use an “API-Gateway/Lambda-proxy-like” event shape where the HTTP request body is always delivered as a string in event.body, plus a boolean flag event.isBase64Encoded that tells you whether that string is base64 for raw/binary bytes. There isn’t a separate “enable base64 for binary bodies” toggle documented; you handle it at runtime by checking isBase64Encoded and decoding when needed. [1], [2]
What to do in your function
- Read
event.body(string) andevent.isBase64Encoded(boolean). [1], [2] - If
isBase64Encoded === true, base64-decodeevent.bodyto get the original bytes; otherwise treat it as text. [1], [2]
Example (Node.js-style pseudocode):
const raw = event.isBase64Encoded
? Buffer.from(event.body, "base64") // binary bytes
: Buffer.from(event.body, "utf8"); // text bytesSources: Scaleway Functions event object docs show body + isBase64Encoded. [1] The official Go events types for Scaleway Functions also model the same Body + IsBase64Encoded fields. [2]
[1] https://www.scaleway.com/en/docs/serverless-functions/reference-content/code-examples/
[2] https://pkg.go.dev/github.com/scaleway/scaleway-functions-go/events
@Siilwyn, great work on fixing the query and header handling! 🎉
Regarding base64 encoding: Scaleway automatically sets isBase64Encoded to true when the request body contains binary data — there's no configuration needed on Scaleway's side. The platform detects binary content (like file uploads, images, PDFs) and automatically base64-encodes it before passing it to your function.
The handler code needs to check this flag and decode accordingly:
body:
event.httpMethod === "GET" || event.httpMethod === "HEAD"
? undefined
: event.isBase64Encoded
? Buffer.from(event.body || "", "base64")
: event.body,This ensures binary uploads work correctly. Without this check, binary data would remain base64-encoded and corrupted when passed to your Nitro app.
Would you like to add this decoding logic? It's a small addition that ensures the preset handles all content types properly.
🧠 Learnings used
Learnt from: medz
Repo: nitrojs/nitro PR: 3834
File: src/presets/cloudflare/server-entry.ts:63-63
Timestamp: 2025-12-03T19:09:13.905Z
Learning: In the Nitro Cloudflare preset (src/presets/cloudflare/server-entry.ts), native errors from oxc-parser and Node.js readFile operations should be allowed to bubble up naturally without wrapping, as their native error messages are more user-friendly and provide better diagnostic information.
If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!
🔗 Linked issue
#2973
❓ Type of change
📚 Description
Add Scaleway serverless functions preset.
📝 Checklist
Other
Open to all feedback, I'm figuring out what to add further to the documentation and preset config so deploys go as easy as possible.