Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .devcontainer/rust/devcontainer-feature.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependsOn": {
"ghcr.io/devcontainers/features/rust:1": {
// this should match the `rust-toolchain.toml`
"version": "nightly-2026-07-29",
"version": "nightly-2026-08-20",
"profile": "minimal",
"components": "rustfmt,clippy,rust-analyzer"
}
Expand Down
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,25 @@ module.exports = {
allowedDevOrigins: ['local-origin.dev', '*.local-origin.dev'],
}
```

Only the [`hostname`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hostname) of the request's `Origin` header is matched against your entries. For a request from `http://local-origin.dev:3000/dashboard?tab=1`, that is `local-origin.dev`. The scheme, the port, the path, and the query string are ignored. Write your entries that way too, without `https://` and without a port.

A no-cors cross-site request, such as a script tag loading a dev asset, sends no `Origin` header. Those are matched on the `Referer` hostname instead.

Entries can also expand, through two wildcards: a `*` stands in for exactly one label of the hostname, and `**` for one or more. That is why the example above lists two entries, one for the bare hostname and one for its subdomains.

| Entry | Matches | Does not match |
| --------------------- | --------------------------------------------------- | ---------------------------------------------- |
| `local-origin.dev` | `local-origin.dev` | `team.local-origin.dev` |
| `*.local-origin.dev` | `team.local-origin.dev` | `local-origin.dev`, `team.eu.local-origin.dev` |
| `**.local-origin.dev` | `team.local-origin.dev`, `team.eu.local-origin.dev` | `local-origin.dev` |

Partial replacement is not supported. Write `*.local-origin.dev`, rather than `team-*.local-origin.dev`. Using `**` is only supported at the start of the pattern.

The dev server already allows `localhost`, its subdomains, and the hostname it was started with. Any other hostname needs an entry, such as a tunnel used for remote development:

```js filename="next.config.js"
module.exports = {
allowedDevOrigins: ['*.tunnel.example.com'],
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Options for configuring Server Actions behavior in your Next.js application. For

## `allowedOrigins`

A list of extra safe origin domains from which Server Actions can be invoked. Next.js compares the origin of a Server Action request with the host domain, ensuring they match to prevent CSRF attacks. If not provided, only the same origin is allowed.
A list of extra safe hosts from which Server Actions can be invoked. To prevent CSRF attacks, Next.js compares the host in a request's `Origin` header against the app's own host, taken from `x-forwarded-host` or `host`, and rejects the action when the two differ. If not provided, only the same origin is allowed. A request that carries no `Origin` header at all is allowed through with a warning rather than rejected.

```js filename="next.config.js"
/** @type {import('next').NextConfig} */
Expand All @@ -24,6 +24,38 @@ module.exports = {
}
```

Only the [`host`](https://developer.mozilla.org/en-US/docs/Web/API/URL/host) of the request's `Origin` header is matched against your entries, which is the hostname plus the port when the URL carries one. For a request from `https://my-proxy.com/checkout`, that is `my-proxy.com`. For one from `https://my-proxy.com:8443/checkout`, `my-proxy.com:8443`. Write your entries the same way.

Entries can also expand, through two wildcards: a `*` stands in for exactly one label of the host, and `**` for one or more. That is why the example above lists two entries, one for the bare host and one for its subdomains.

| Entry | Matches | Does not match |
| ------------------- | ----------------------------------------- | --------------------------------------- |
| `my-proxy.com` | `my-proxy.com` | `my-proxy.com:8443`, `app.my-proxy.com` |
| `*.my-proxy.com` | `app.my-proxy.com` | `my-proxy.com`, `app.my-proxy.com:8443` |
| `**.my-proxy.com` | `app.my-proxy.com`, `app.eu.my-proxy.com` | `my-proxy.com` |
| `my-proxy.com:8443` | `my-proxy.com:8443` | `my-proxy.com` |

Partial replacement is not supported. Write `*.my-proxy.com`, rather than `app-*.my-proxy.com`. Using `**` is only supported at the start of the pattern. A port cannot be wildcarded, so write it out in full: `my-proxy.com:8443`, or `*.my-proxy.com:8443` for its subdomains.

Behind a reverse proxy, no entry is needed as long as the proxy forwards the public host in `x-forwarded-host`. When it forwards its own host instead, the browser sends `my-proxy.com` while the server reports something like `localhost:3000`, and that mismatch is what this list is for: the host visible in the browser's address bar is the one to add, not the internal one the server reports.

The check runs in production as well as in development, so the list applies to your deployed app and not only to local work.

For example, remote development through a tunnel needs the tunnel hostname in two places: in [`allowedDevOrigins`](/docs/app/api-reference/config/next-config-js/allowedDevOrigins), so the dev server serves its own assets and endpoints, and here, so actions from it are accepted:

```js filename="next.config.js"
/** @type {import('next').NextConfig} */

module.exports = {
allowedDevOrigins: ['*.tunnel.example.com'],
experimental: {
serverActions: {
allowedOrigins: ['*.tunnel.example.com'],
},
},
}
```

## `bodySizeLimit`

By default, the maximum size of the request body sent to a Server Action is 1MB, to prevent the consumption of excessive server resources in parsing large amounts of data, as well as potential DDoS attacks.
Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# if you update this, also update `.devcontainer/rust/devcontainer-feature.json`
# if you move the file, also update any turbo.json inputs that references it.
[toolchain]
channel = "nightly-2026-07-29"
channel = "nightly-2026-08-20"
components = ["rustfmt", "clippy", "rust-analyzer"]
profile = "minimal"
5 changes: 4 additions & 1 deletion turbopack/crates/turbo-rcstr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ napi = { workspace = true, optional = true }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
scattered-collect = { workspace = true }

[dev-dependencies]
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
# Only used by the benchmarks, which don't run on wasm. Gated by target so that
# `cargo test -p turbo-rcstr --lib --target wasm32-wasip1-threads` can build: criterion pulls in
# rayon, which refuses to compile for wasi, and cargo resolves dev-dependencies even for `--lib`.
criterion = { workspace = true }

[lints]
Expand Down
62 changes: 62 additions & 0 deletions turbopack/crates/turbo-rcstr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,65 @@ mod tests {
}
};
assert_eq!(STR, RcStr::from("hello"));

// A literal one byte past the capacity must not inline, on either width.
let too_long = "x".repeat(MAX_INLINE_LEN + 1);
assert!(inline_atom(&too_long).is_none());
}

/// The inline capacity must be the same on every target. `turbo-rcstr-macros` runs on the
/// *host*, so it decides inline-vs-static using a host-side constant; if the target disagreed,
/// the macro would emit `inline_atom(..).unwrap()` for a literal that does not fit and panic at
/// runtime. This is the regression guard for that.
#[test]
#[cfg(not(feature = "atom_size_128"))]
fn max_inline_len_is_uniform_across_targets() {
assert_eq!(
MAX_INLINE_LEN, 7,
"MAX_INLINE_LEN must be 7 on every target, including 32-bit/wasm"
);
assert_eq!(size_of::<crate::tagged_value::TaggedValue>(), 8);
// The non-zero niche must survive, or `Option<RcStr>` silently doubles in size.
assert_eq!(size_of::<Option<RcStr>>(), size_of::<RcStr>());
}

/// `rcstr!` expands to a `const`, so it must stay const-evaluable on every target. On 32-bit
/// this only works because `TaggedValue` holds the address in a real pointer field: a bare
/// integer representation would need a pointer→integer cast, which const evaluation forbids,
/// and every literal taking the static path would fail with `E0080`.
#[test]
fn rcstr_macro_is_const_on_every_target() {
// Short enough to be stored inline.
const SHORT: RcStr = rcstr!("abc");
// Longer than the inline capacity, so this takes the static path — the one that needs the
// pointer to survive const evaluation.
const LONG: RcStr = rcstr!("a string that is definitely not inline");

assert_eq!(SHORT, RcStr::from("abc"));
assert_eq!(LONG, RcStr::from("a string that is definitely not inline"));
assert_eq!(SHORT.tag(), INLINE_TAG);
assert_eq!(LONG.tag(), STATIC_TAG);
}

/// Round-trips across the inline/static boundary. Lengths 4..=7 are the interesting band: they
/// are inline at capacity 7 but would spill to the static path at capacity 3, so this fails if
/// the representation ever diverges by target again.
#[test]
fn round_trip_across_the_inline_boundary() {
for len in 0..=9usize {
let s = "abcdefghi"[..len].to_string();
let r = RcStr::from(s.as_str());
assert_eq!(r.as_str(), s, "round trip failed at len {len}");
assert_eq!(r.len(), len);

let expected_inline = len <= MAX_INLINE_LEN;
assert_eq!(
r.tag() == INLINE_TAG,
expected_inline,
"len {len} should {} be inline (MAX_INLINE_LEN = {MAX_INLINE_LEN})",
if expected_inline { "" } else { "not" }
);
}
}

#[test]
Expand Down Expand Up @@ -833,6 +892,9 @@ mod tests {
}

#[test]
// `STATIC_RCSTRS` is an empty array on wasm (see its definition above), so there is no static
// registry for the decoder to resolve against and the value comes back as `DYNAMIC_TAG`.
#[cfg_attr(target_family = "wasm", ignore = "no static RcStr registry on wasm")]
fn test_bincode_roundtrip() {
use turbo_bincode::{turbo_bincode_decode, turbo_bincode_encode};

Expand Down
Loading
Loading