2021-10-20 19:52:11 +02:00
|
|
|
import type { NodeHeaders } from './types'
|
|
|
|
|
2021-10-26 05:26:28 +02:00
|
|
|
export function fromNodeHeaders(object: NodeHeaders): Headers {
|
|
|
|
const headers = new Headers()
|
|
|
|
for (let [key, value] of Object.entries(object)) {
|
|
|
|
const values = Array.isArray(value) ? value : [value]
|
|
|
|
for (let v of values) {
|
|
|
|
if (v !== undefined) {
|
|
|
|
headers.append(key, v)
|
|
|
|
}
|
2021-10-20 19:52:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return headers
|
|
|
|
}
|
|
|
|
|
|
|
|
export function toNodeHeaders(headers?: Headers): NodeHeaders {
|
2021-10-26 05:26:28 +02:00
|
|
|
const result: NodeHeaders = {}
|
2021-10-20 19:52:11 +02:00
|
|
|
if (headers) {
|
|
|
|
for (const [key, value] of headers.entries()) {
|
2021-10-26 05:26:28 +02:00
|
|
|
result[key] = value
|
|
|
|
if (key.toLowerCase() === 'set-cookie') {
|
Split Set-Cookie header correctly (#30560)
## Bug
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`
Fixes #30430
There's some more discussion in the issue, but in summary:
- web `Headers` implementation combines all header values with `', '`
- For `Set-Cookie` headers, you're supposed to set them as separate values, not combine them
- web `Headers` forbids the use of `Cookie`, `Set-Cookie` and some more headers, so they don't have custom implementation for those, and still joins them with `,`
- We currently just split them using `split(',')`, but this breaks when the header contains a date (expires, max-age) that also includes a `,`
I used this method to split the Set-Cookie header properly: https://www.npmjs.com/package/set-cookie-parser#splitcookiestringcombinedsetcookieheader as suggested [here](https://github.com/whatwg/fetch/issues/973#issuecomment-559678813)
I didn't add it as a dependency, since we only needed that one method and I wasn't sure what the process is for adding dependencies, so I just added the method in the middleware utils
2021-10-28 19:46:58 +02:00
|
|
|
result[key] = splitCookiesString(value)
|
2021-10-26 05:26:28 +02:00
|
|
|
}
|
2021-10-20 19:52:11 +02:00
|
|
|
}
|
|
|
|
}
|
2021-10-26 05:26:28 +02:00
|
|
|
return result
|
2021-10-20 19:52:11 +02:00
|
|
|
}
|
Split Set-Cookie header correctly (#30560)
## Bug
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`
Fixes #30430
There's some more discussion in the issue, but in summary:
- web `Headers` implementation combines all header values with `', '`
- For `Set-Cookie` headers, you're supposed to set them as separate values, not combine them
- web `Headers` forbids the use of `Cookie`, `Set-Cookie` and some more headers, so they don't have custom implementation for those, and still joins them with `,`
- We currently just split them using `split(',')`, but this breaks when the header contains a date (expires, max-age) that also includes a `,`
I used this method to split the Set-Cookie header properly: https://www.npmjs.com/package/set-cookie-parser#splitcookiestringcombinedsetcookieheader as suggested [here](https://github.com/whatwg/fetch/issues/973#issuecomment-559678813)
I didn't add it as a dependency, since we only needed that one method and I wasn't sure what the process is for adding dependencies, so I just added the method in the middleware utils
2021-10-28 19:46:58 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas
|
|
|
|
that are within a single set-cookie field-value, such as in the Expires portion.
|
|
|
|
This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2
|
|
|
|
Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128
|
|
|
|
React Native's fetch does this for *every* header, including set-cookie.
|
|
|
|
|
|
|
|
Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25
|
|
|
|
Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation
|
|
|
|
*/
|
|
|
|
export function splitCookiesString(cookiesString: string) {
|
|
|
|
var cookiesStrings = []
|
|
|
|
var pos = 0
|
|
|
|
var start
|
|
|
|
var ch
|
|
|
|
var lastComma
|
|
|
|
var nextStart
|
|
|
|
var cookiesSeparatorFound
|
|
|
|
|
|
|
|
function skipWhitespace() {
|
|
|
|
while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
|
|
|
|
pos += 1
|
|
|
|
}
|
|
|
|
return pos < cookiesString.length
|
|
|
|
}
|
|
|
|
|
|
|
|
function notSpecialChar() {
|
|
|
|
ch = cookiesString.charAt(pos)
|
|
|
|
|
|
|
|
return ch !== '=' && ch !== ';' && ch !== ','
|
|
|
|
}
|
|
|
|
|
|
|
|
while (pos < cookiesString.length) {
|
|
|
|
start = pos
|
|
|
|
cookiesSeparatorFound = false
|
|
|
|
|
|
|
|
while (skipWhitespace()) {
|
|
|
|
ch = cookiesString.charAt(pos)
|
|
|
|
if (ch === ',') {
|
|
|
|
// ',' is a cookie separator if we have later first '=', not ';' or ','
|
|
|
|
lastComma = pos
|
|
|
|
pos += 1
|
|
|
|
|
|
|
|
skipWhitespace()
|
|
|
|
nextStart = pos
|
|
|
|
|
|
|
|
while (pos < cookiesString.length && notSpecialChar()) {
|
|
|
|
pos += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// currently special character
|
|
|
|
if (pos < cookiesString.length && cookiesString.charAt(pos) === '=') {
|
|
|
|
// we found cookies separator
|
|
|
|
cookiesSeparatorFound = true
|
|
|
|
// pos is inside the next cookie, so back up and return it.
|
|
|
|
pos = nextStart
|
|
|
|
cookiesStrings.push(cookiesString.substring(start, lastComma))
|
|
|
|
start = pos
|
|
|
|
} else {
|
|
|
|
// in param ',' or param separator ';',
|
|
|
|
// we continue from that comma
|
|
|
|
pos = lastComma + 1
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
pos += 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!cookiesSeparatorFound || pos >= cookiesString.length) {
|
|
|
|
cookiesStrings.push(cookiesString.substring(start, cookiesString.length))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cookiesStrings
|
|
|
|
}
|
Enforce absolute URLs in Edge Functions runtime (#33410)
We currently have inconsistencies when working with URLs in the Edge Functions runtime, this PR addresses them introducing a warning for inconsistent usage that will break in the future. Here is the reasoning.
### The Browser
When we are in a browser environment there is a fixed location stored at `globalThis.location`. Then, if one tries to build a request with a relative URL it will work using that location global hostname as _base_ to construct its URL. For example:
```typescript
// https://nextjs.org
new Request('/test').url; // https://nextjs.org/test
Response.redirect('/test').headers.get('Location'); // https://nextjs.org/test
```
However, if we attempt to run the same code from `about:blank` it would not work because the global to use as a base `String(globalThis.location)` is not a valid URL. Therefore a call to `Response.redirect('/test')` or `new Response('/test')` would fail.
### Edge Functions Runtime
In Next.js Edge Functions runtime the situation is slightly different from a browser. Say that we have a root middleware (`pages/_middleware`) that gets invoked for every page. In the middleware file we expose the handler function and also define a global variable that we mutate on every request:
```typescript
// pages/_middleware
let count = 0;
export function middleware(req: NextRequest) {
console.log(req.url);
count += 1;
}
```
Currently we cache the module scope in the runtime so subsequent invocations would hold the same globals and the module would not be evaluated again. This would make the counter to increment for each request that the middleware handles. It is for this reason that we **can't have a global location** that changes across different invocations. Each invocation of the same function uses the same global which also holds primitives like `URL` or `Request` so changing an hypothetical `globalThis.location` per request would affect concurrent requests being handled.
Then, it is not possible to use relative URLs in the same way the browser does because we don't have a global to rely on to use its host to compose a URL from a relative path.
### Why it works today
We are **not** validating what is provided to, for example, `NextResponse.rewrite()` nor `NextResponse.redirect()`. We simply create a `Response` instance that adds the corresponding header for the rewrite or the redirect. Then it is **the consumer** the one that composes the final destination based on the request. Theoretically you can pass any value and it would fail on redirect but won't validate the input.
Of course this is inconsistent because it doesn't make sense that `NextResponse.rewrite('/test')` works but `fetch(new NextRequest('/test'))` does not. Also we should validate what is provided. Finally, we want to be consistent with the way a browser behaves so `new Request('/test')` _should_ not work if there is no global location which we lack.
### What this PR does
We will have to deprecate the usage of relative URLs in the previously mentioned scenarios. In preparation for it, this PR adds a validation function in those places where it will break in the future, printing a warning with a link that points to a Next.js page with an explanation of the issue and ways to fix it.
Although middleware changes are not covered by semver, we will roll this for some time to make people aware that this change is coming. Then after a reasonable period of time we can remove the warning and make the code fail when using relative URLs in the previously exposed scenarios.
2022-01-19 16:10:25 +01:00
|
|
|
|
|
|
|
/**
|
2022-02-17 16:12:36 +01:00
|
|
|
* Validate the correctness of a user-provided URL.
|
Enforce absolute URLs in Edge Functions runtime (#33410)
We currently have inconsistencies when working with URLs in the Edge Functions runtime, this PR addresses them introducing a warning for inconsistent usage that will break in the future. Here is the reasoning.
### The Browser
When we are in a browser environment there is a fixed location stored at `globalThis.location`. Then, if one tries to build a request with a relative URL it will work using that location global hostname as _base_ to construct its URL. For example:
```typescript
// https://nextjs.org
new Request('/test').url; // https://nextjs.org/test
Response.redirect('/test').headers.get('Location'); // https://nextjs.org/test
```
However, if we attempt to run the same code from `about:blank` it would not work because the global to use as a base `String(globalThis.location)` is not a valid URL. Therefore a call to `Response.redirect('/test')` or `new Response('/test')` would fail.
### Edge Functions Runtime
In Next.js Edge Functions runtime the situation is slightly different from a browser. Say that we have a root middleware (`pages/_middleware`) that gets invoked for every page. In the middleware file we expose the handler function and also define a global variable that we mutate on every request:
```typescript
// pages/_middleware
let count = 0;
export function middleware(req: NextRequest) {
console.log(req.url);
count += 1;
}
```
Currently we cache the module scope in the runtime so subsequent invocations would hold the same globals and the module would not be evaluated again. This would make the counter to increment for each request that the middleware handles. It is for this reason that we **can't have a global location** that changes across different invocations. Each invocation of the same function uses the same global which also holds primitives like `URL` or `Request` so changing an hypothetical `globalThis.location` per request would affect concurrent requests being handled.
Then, it is not possible to use relative URLs in the same way the browser does because we don't have a global to rely on to use its host to compose a URL from a relative path.
### Why it works today
We are **not** validating what is provided to, for example, `NextResponse.rewrite()` nor `NextResponse.redirect()`. We simply create a `Response` instance that adds the corresponding header for the rewrite or the redirect. Then it is **the consumer** the one that composes the final destination based on the request. Theoretically you can pass any value and it would fail on redirect but won't validate the input.
Of course this is inconsistent because it doesn't make sense that `NextResponse.rewrite('/test')` works but `fetch(new NextRequest('/test'))` does not. Also we should validate what is provided. Finally, we want to be consistent with the way a browser behaves so `new Request('/test')` _should_ not work if there is no global location which we lack.
### What this PR does
We will have to deprecate the usage of relative URLs in the previously mentioned scenarios. In preparation for it, this PR adds a validation function in those places where it will break in the future, printing a warning with a link that points to a Next.js page with an explanation of the issue and ways to fix it.
Although middleware changes are not covered by semver, we will roll this for some time to make people aware that this change is coming. Then after a reasonable period of time we can remove the warning and make the code fail when using relative URLs in the previously exposed scenarios.
2022-01-19 16:10:25 +01:00
|
|
|
*/
|
|
|
|
export function validateURL(url: string | URL): string {
|
|
|
|
try {
|
|
|
|
return String(new URL(String(url)))
|
|
|
|
} catch (error: any) {
|
2022-02-17 16:12:36 +01:00
|
|
|
throw new Error(
|
|
|
|
`URLs is malformed. Please use only absolute URLs - https://nextjs.org/docs/messages/middleware-relative-urls`,
|
|
|
|
{ cause: error }
|
Enforce absolute URLs in Edge Functions runtime (#33410)
We currently have inconsistencies when working with URLs in the Edge Functions runtime, this PR addresses them introducing a warning for inconsistent usage that will break in the future. Here is the reasoning.
### The Browser
When we are in a browser environment there is a fixed location stored at `globalThis.location`. Then, if one tries to build a request with a relative URL it will work using that location global hostname as _base_ to construct its URL. For example:
```typescript
// https://nextjs.org
new Request('/test').url; // https://nextjs.org/test
Response.redirect('/test').headers.get('Location'); // https://nextjs.org/test
```
However, if we attempt to run the same code from `about:blank` it would not work because the global to use as a base `String(globalThis.location)` is not a valid URL. Therefore a call to `Response.redirect('/test')` or `new Response('/test')` would fail.
### Edge Functions Runtime
In Next.js Edge Functions runtime the situation is slightly different from a browser. Say that we have a root middleware (`pages/_middleware`) that gets invoked for every page. In the middleware file we expose the handler function and also define a global variable that we mutate on every request:
```typescript
// pages/_middleware
let count = 0;
export function middleware(req: NextRequest) {
console.log(req.url);
count += 1;
}
```
Currently we cache the module scope in the runtime so subsequent invocations would hold the same globals and the module would not be evaluated again. This would make the counter to increment for each request that the middleware handles. It is for this reason that we **can't have a global location** that changes across different invocations. Each invocation of the same function uses the same global which also holds primitives like `URL` or `Request` so changing an hypothetical `globalThis.location` per request would affect concurrent requests being handled.
Then, it is not possible to use relative URLs in the same way the browser does because we don't have a global to rely on to use its host to compose a URL from a relative path.
### Why it works today
We are **not** validating what is provided to, for example, `NextResponse.rewrite()` nor `NextResponse.redirect()`. We simply create a `Response` instance that adds the corresponding header for the rewrite or the redirect. Then it is **the consumer** the one that composes the final destination based on the request. Theoretically you can pass any value and it would fail on redirect but won't validate the input.
Of course this is inconsistent because it doesn't make sense that `NextResponse.rewrite('/test')` works but `fetch(new NextRequest('/test'))` does not. Also we should validate what is provided. Finally, we want to be consistent with the way a browser behaves so `new Request('/test')` _should_ not work if there is no global location which we lack.
### What this PR does
We will have to deprecate the usage of relative URLs in the previously mentioned scenarios. In preparation for it, this PR adds a validation function in those places where it will break in the future, printing a warning with a link that points to a Next.js page with an explanation of the issue and ways to fix it.
Although middleware changes are not covered by semver, we will roll this for some time to make people aware that this change is coming. Then after a reasonable period of time we can remove the warning and make the code fail when using relative URLs in the previously exposed scenarios.
2022-01-19 16:10:25 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|