Skip to content

Commit

Permalink
[@vercel/edge] Support overriding request headers (#8724)
Browse files Browse the repository at this point in the history
<!--
Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change that you're making:
-->

This PR adds a feature in middleware to add, modify, or delete request
headers. This feature is quite useful to pass data from middleware to
Serverless/Edge API routes.

### New APIs

Adds a new option `request.headers` to the `MiddlewareResponseInit`
parameter in `NextResponse.next()` and `NextResponse.rewrite()`. It's a
[`Header`](https://developer.mozilla.org/en-US/docs/Web/API/Headers)
object holding *all* request headers. Specifically:

```ts
interface ExtraResponseInit extends ResponseInit {
  request?: {
    headers?: Headers
  }
}
```

### Example

```ts
// api/hello.ts
export default (req, res) => {
  const valueFromMiddleware = req.headers['x-hello-from-middleware']
  return res.send(valueFromMiddleware)
}

// middleware.ts
import { next } from '@vercel/edge'

export default function middleware(request: NextRequest) {
  // Clone request headers
  const headers = new Headers(request.headers);
  // Add a new request header
  headers.set('x-hello-from-middleware', 'foo');
  // Delete a request header from the client
  headers.delete('x-from-client');

  return next({
    request: {
      headers
    }
  });
}
```

### New middleware headers

- `x-middleware-override-headers`: A comma separated list of *all*
request header names. Headers not listed will be deleted.
- `x-middleware-request-<name>`: A new value for the header `<name>`.

### Related Issues

- Next.js' implementation: vercel/next.js#41380

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with
tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a
reviewer
- [ ] Issue from task tracker has a link to this PR

Co-authored-by: Gal Schlezinger <gal@spitfire.co.il>
  • Loading branch information
nuta and Schniz committed Oct 21, 2022
1 parent b388357 commit df9accf
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 3 deletions.
5 changes: 3 additions & 2 deletions packages/edge/docs/README.md
Expand Up @@ -6,6 +6,7 @@

- [ExtraResponseInit](interfaces/ExtraResponseInit.md)
- [Geo](interfaces/Geo.md)
- [ModifiedRequest](interfaces/ModifiedRequest.md)

### Variables

Expand Down Expand Up @@ -212,7 +213,7 @@ export default function middleware(_req: Request) {

#### Defined in

[src/middleware-helpers.ts:94](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L94)
[src/middleware-helpers.ts:145](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L145)

---

Expand Down Expand Up @@ -274,4 +275,4 @@ export const config = { matcher: '/api/users/:path*' };

#### Defined in

[src/middleware-helpers.ts:53](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L53)
[src/middleware-helpers.ts:101](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L101)
15 changes: 14 additions & 1 deletion packages/edge/docs/interfaces/ExtraResponseInit.md
Expand Up @@ -11,6 +11,7 @@
### Properties

- [headers](ExtraResponseInit.md#headers)
- [request](ExtraResponseInit.md#request)
- [status](ExtraResponseInit.md#status)
- [statusText](ExtraResponseInit.md#statustext)

Expand All @@ -25,7 +26,19 @@ along with the response headers from the origin.

#### Defined in

[src/middleware-helpers.ts:6](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L6)
[src/middleware-helpers.ts:31](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L31)

---

### request

`Optional` **request**: [`ModifiedRequest`](ModifiedRequest.md)

Fields to rewrite for the upstream request.

#### Defined in

[src/middleware-helpers.ts:35](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L35)

---

Expand Down
38 changes: 38 additions & 0 deletions packages/edge/docs/interfaces/ModifiedRequest.md
@@ -0,0 +1,38 @@
# Interface: ModifiedRequest

## Table of contents

### Properties

- [headers](ModifiedRequest.md#headers)

## Properties

### headers

`Optional` **headers**: `Headers`

If set, overwrites the incoming headers to the origin request.

This is useful when you want to pass data between a Middleware and a
Serverless or Edge Function.

**`Example`**

<caption>Add a `x-user-id` header and remove the `Authorization` header</caption>

```ts
import { rewrite } from '@vercel/edge';
export default async function middleware(request: Request): Promise<Response> {
const newHeaders = new Headers(request.headers);
newHeaders.set('x-user-id', 'user_123');
newHeaders.delete('authorization');
return rewrite(request.url, {
request: { headers: newHeaders },
});
}
```

#### Defined in

[src/middleware-helpers.ts:23](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L23)
54 changes: 54 additions & 0 deletions packages/edge/src/middleware-helpers.ts
@@ -1,9 +1,57 @@
export interface ModifiedRequest {
/**
* If set, overwrites the incoming headers to the origin request.
*
* This is useful when you want to pass data between a Middleware and a
* Serverless or Edge Function.
*
* @example
* <caption>Add a `x-user-id` header and remove the `Authorization` header</caption>
*
* ```ts
* import { rewrite } from '@vercel/edge';
* export default async function middleware(request: Request): Promise<Response> {
* const newHeaders = new Headers(request.headers);
* newHeaders.set('x-user-id', 'user_123');
* newHeaders.delete('authorization');
* return rewrite(request.url, {
* request: { headers: newHeaders }
* })
* }
* ```
*/
headers?: Headers;
}

export interface ExtraResponseInit extends Omit<ResponseInit, 'headers'> {
/**
* These headers will be sent to the user response
* along with the response headers from the origin.
*/
headers?: HeadersInit;
/**
* Fields to rewrite for the upstream request.
*/
request?: ModifiedRequest;
}

function handleMiddlewareField(
init: ExtraResponseInit | undefined,
headers: Headers
) {
if (init?.request?.headers) {
if (!(init.request.headers instanceof Headers)) {
throw new Error('request.headers must be an instance of Headers');
}

const keys = [];
for (const [key, value] of init.request.headers) {
headers.set('x-middleware-request-' + key, value);
keys.push(key);
}

headers.set('x-middleware-override-headers', keys.join(','));
}
}

/**
Expand Down Expand Up @@ -56,6 +104,9 @@ export function rewrite(
): Response {
const headers = new Headers(init?.headers ?? {});
headers.set('x-middleware-rewrite', String(destination));

handleMiddlewareField(init, headers);

return new Response(null, {
...init,
headers,
Expand Down Expand Up @@ -94,6 +145,9 @@ export function rewrite(
export function next(init?: ExtraResponseInit): Response {
const headers = new Headers(init?.headers ?? {});
headers.set('x-middleware-next', '1');

handleMiddlewareField(init, headers);

return new Response(null, {
...init,
headers,
Expand Down
52 changes: 52 additions & 0 deletions packages/edge/test/middleware-helpers.test.ts
Expand Up @@ -22,6 +22,32 @@ describe('rewrite', () => {
},
});
});

test('receives new request headers', () => {
const headers = new Headers();
headers.set('x-from-middleware1', 'hello1');
headers.set('x-from-middleware2', 'hello2');
const resp = rewrite(new URL('https://example.vercel.sh/'), {
headers: {
'x-custom-header': 'custom-value',
},
request: { headers },
});
expect({
status: resp.status,
headers: Object.fromEntries(resp.headers),
}).toMatchObject({
status: 200,
headers: {
'x-middleware-rewrite': 'https://example.vercel.sh/',
'x-custom-header': 'custom-value',
'x-middleware-override-headers':
'x-from-middleware1,x-from-middleware2',
'x-middleware-request-x-from-middleware2': 'hello2',
'x-middleware-request-x-from-middleware1': 'hello1',
},
});
});
});

describe('next', () => {
Expand All @@ -42,4 +68,30 @@ describe('next', () => {
},
});
});

test('receives new request headers', () => {
const headers = new Headers();
headers.set('x-from-middleware1', 'hello1');
headers.set('x-from-middleware2', 'hello2');
const resp = next({
headers: {
'x-custom-header': 'custom-value',
},
request: { headers },
});
expect({
status: resp.status,
headers: Object.fromEntries(resp.headers),
}).toMatchObject({
status: 200,
headers: {
'x-middleware-next': '1',
'x-custom-header': 'custom-value',
'x-middleware-override-headers':
'x-from-middleware1,x-from-middleware2',
'x-middleware-request-x-from-middleware2': 'hello2',
'x-middleware-request-x-from-middleware1': 'hello1',
},
});
});
});

0 comments on commit df9accf

Please sign in to comment.