Skip to content

feat: Add Scaleway serverless function preset#3970

Open
Siilwyn wants to merge 3 commits intonitrojs:mainfrom
Siilwyn:scaleway
Open

feat: Add Scaleway serverless function preset#3970
Siilwyn wants to merge 3 commits intonitrojs:mainfrom
Siilwyn:scaleway

Conversation

@Siilwyn
Copy link

@Siilwyn Siilwyn commented Jan 23, 2026

🔗 Linked issue

#2973

❓ Type of change

  • 📖 Documentation (updates to the documentation, readme, or JSdoc annotations)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

Add Scaleway serverless functions preset.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

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.

@vercel
Copy link

vercel bot commented Jan 23, 2026

@Siilwyn is attempting to deploy a commit to the Nitro Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Scaleway preset & runtime
src/presets/scaleway/preset.ts, src/presets/scaleway/runtime/scaleway-serverless.ts
New preset scaleway-serverless and runtime handler that normalizes Scaleway events into HTTP Requests and forwards them to the Nitro app via nitroApp.fetch.
Preset registry
src/presets/_all.gen.ts
Imports and includes the new Scaleway preset in the aggregated presets export.
Type updates
src/presets/_types.gen.ts
Extends PresetName to include "scaleway-serverless" and PresetNameInput to accept "scaleway-serverless", "scalewayServerless", and "scaleway_serverless".
Documentation
docs/2.deploy/20.providers/scaleway.md
New documentation page describing the Scaleway Serverless deployment preset.
Tests
test/presets/scaleway.test.ts
New integration test that builds/loads the handler, starts a local HTTP server on a random port, and proxies fetch requests to validate the runtime.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: Add Scaleway serverless function preset' follows conventional commits format with appropriate 'feat:' prefix and clearly describes the main change.
Description check ✅ Passed The PR description is directly related to the changeset, providing context about adding a Scaleway serverless preset with linked issue, change type categorization, and checklist completion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Siilwyn Siilwyn force-pushed the scaleway branch 2 times, most recently from dfc067b to 1b2a136 Compare January 23, 2026 15:17
@Siilwyn Siilwyn marked this pull request as ready for review January 23, 2026 15:24
@Siilwyn Siilwyn requested a review from pi0 as a code owner January 23, 2026 15:24
@Siilwyn
Copy link
Author

Siilwyn commented Jan 23, 2026

Moved from draft so CodeRabbit picks it up.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.config example 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/"}

Comment on lines 11 to 29
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,
});
Copy link

@coderabbitai coderabbitai bot Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the Scaleway adapter structure
find . -type f -name "*scaleway*" | head -20

Repository: 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.ts

Repository: 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 -10

Repository: 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/eventsAPIGatewayProxyRequest fields (pkg.go.dev)

Citations:


🏁 Script executed:

# Check if `@scaleway/serverless-functions` is in package.json or node_modules
grep -r "scaleway/serverless-functions" . --include="*.json" --include="*.ts" | head -10

Repository: nitrojs/nitro

Length of output: 273


🏁 Script executed:

# Look at the test file for Scaleway adapter
cat -n test/presets/scaleway.test.ts

Repository: 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 -10

Repository: 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 -60

Repository: 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 -20

Repository: 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.ts

Repository: 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 isBase64Encoded is 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.

Suggested change
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.

Copy link
Author

@Siilwyn Siilwyn Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since all tests pass I don't think this is needed?

  • event.queryStringParameters can be null because withQuery handles this.
  • The expected headers are set by the Scaleway runtime afaik, so X-Forwarded-Proto should always be in this casing.
  • Maybe there are no tests for isBase64Encoded, is this expected to work?
  • Are HEAD requests without a body also not tested?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 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) and withQuery("/x", undefined) throw (because spreading null/undefined into an object is a TypeError). [1]
  • For individual query values when query is an object:
    • undefined values are omitted entirely (key removed). [1]
    • null (and "") become a key without = (e.g. { a: null }?a). [1]
    • 0 / false are 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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 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

  1. Read event.body (string) and event.isBase64Encoded (boolean). [1], [2]
  2. If isBase64Encoded === true, base64-decode event.body to 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 bytes

Sources: 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant