Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/2.deploy/20.providers/scaleway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Scaleway

> Deploy Nitro apps to Scaleway.
**Preset:** `scaleway-serverless`

:read-more{title="Scaleway Serverless Functions" to="https://www.scaleway.com/en/docs/serverless-functions/"}
2 changes: 2 additions & 0 deletions src/presets/_all.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import _netlify from "./netlify/preset.ts";
import _node from "./node/preset.ts";
import _platformSh from "./platform.sh/preset.ts";
import _renderCom from "./render.com/preset.ts";
import _scaleway from "./scaleway/preset.ts";
import _standard from "./standard/preset.ts";
import _stormkit from "./stormkit/preset.ts";
import _vercel from "./vercel/preset.ts";
Expand Down Expand Up @@ -50,6 +51,7 @@ export default [
..._node,
..._platformSh,
..._renderCom,
..._scaleway,
..._standard,
..._stormkit,
..._vercel,
Expand Down
4 changes: 2 additions & 2 deletions src/presets/_types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ export interface PresetOptions {

export const presetsWithConfig = ["awsAmplify","awsLambda","azure","cloudflare","firebase","netlify","vercel"] as const;

export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-pages" | "cloudflare-pages-static" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis-handler" | "iis-node" | "koyeb" | "netlify" | "netlify-edge" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-middleware" | "node-server" | "platform-sh" | "render-com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static";
export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-pages" | "cloudflare-pages-static" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis-handler" | "iis-node" | "koyeb" | "netlify" | "netlify-edge" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-middleware" | "node-server" | "platform-sh" | "render-com" | "scaleway-serverless" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static";

export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflareDev" | "cloudflare_dev" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "firebase-app-hosting" | "firebaseAppHosting" | "firebase_app_hosting" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "netlify" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-middleware" | "nodeMiddleware" | "node_middleware" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {});
export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflareDev" | "cloudflare_dev" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "firebase-app-hosting" | "firebaseAppHosting" | "firebase_app_hosting" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "netlify" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-middleware" | "nodeMiddleware" | "node_middleware" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "scaleway-serverless" | "scalewayServerless" | "scaleway_serverless" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {});
18 changes: 18 additions & 0 deletions src/presets/scaleway/preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineNitroPreset } from "../_utils/preset.ts";

const scalewayServerless = defineNitroPreset(
{
entry: "./scaleway/runtime/scaleway-serverless",
rollupConfig: {
output: {
entryFileNames: "index.mjs",
format: "esm",
},
},
},
{
name: "scaleway-serverless" as const,
}
);

export default [scalewayServerless] as const;
33 changes: 33 additions & 0 deletions src/presets/scaleway/runtime/scaleway-serverless.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import "#nitro/virtual/polyfills";
import { useNitroApp } from "nitro/app";
import { joinURL, withQuery } from "ufo";
import type { serveHandler } from "@scaleway/serverless-functions";

const nitroApp = useNitroApp();

type Event = Parameters<Parameters<typeof serveHandler>[0]>[0];
type Context = Parameters<Parameters<typeof serveHandler>[0]>[1];

export async function handler(event: Event, context: Context) {
const headers = new Headers(
Object.fromEntries(
Object.entries(event.headers!).map(([key, value]) => [key, String(value)])
)
);

const url = withQuery(
joinURL(
headers.get("X-Forwarded-Proto") === "http" ? "http://" : "https://",
headers.get("Host")!,
event.path
),
event.queryStringParameters ?? {}
);

const request = new Request(url, {
method: event.httpMethod,
headers,
body: event.httpMethod === "GET" ? undefined : event.body,
});
Comment on lines 11 to 31
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/events – APIGatewayProxyRequest 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!

return nitroApp.fetch(request);
}
27 changes: 27 additions & 0 deletions test/presets/scaleway.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe } from "vitest";
import { resolve } from "pathe";
import { serveHandler } from "@scaleway/serverless-functions";
import { getRandomPort, waitForPort } from "get-port-please";
import { setupTest, testNitro } from "../tests.ts";

describe("nitro:preset:scaleway", async () => {
const ctx = await setupTest("scaleway-serverless");

testNitro(ctx, async () => {
const { handler } = await import(resolve(ctx.outDir, "server/index.mjs"));
const port = await getRandomPort();
const server = serveHandler(handler, port);

ctx.server = {
url: `http://127.0.0.1:${port}`,
close: () => server.close(),
};

await waitForPort(port, { host: "127.0.0.1" });

return async ({ url, ...options }) => {
const response = await ctx.fetch(url, options);
return response;
};
});
});