Skip to content

Commit

Permalink
feat: implement modifyHeaders function (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
cyco130 authored Nov 4, 2024
1 parent 1b179cb commit 5f4e686
Show file tree
Hide file tree
Showing 10 changed files with 11,495 additions and 5,930 deletions.
28 changes: 27 additions & 1 deletion packages/base/headers/readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
# `@hattip/headers`

Header parsing and content negotiation utilities for Hattip.
Header manipulation, parsing, and content negotiation utilities for Hattip.

## `modifyHeaders`

Some `Response` objects have an immutable `headers` property. `Response.redirect()`, for instance, creates such an object. When the headers are immutable, a naive middleware to modify the headers will fail:

```js
async function naivePoweredBy(ctx) {
const response = await ctx.next();
// The following line will throw an error if `response.headers` is immutable
response.headers.set("X-Powered-By", "Hattip");
return response;
}
```

The `modifyHeaders` function solves this problem by trying to modify the headers in place, and if that fails, creating a new `Response` object and trying again:

```js
async function correctPoweredBy(ctx) {
let response = await ctx.next();
response = modifyHeaders(response, (headers) => {
headers.set("X-Powered-By", "Hattip");
});

return response;
}
```

## `parseHeaderValue`

Expand Down
2 changes: 2 additions & 0 deletions packages/base/headers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export type { ParsedHeaderValue } from "./parser";
export { accept } from "./accept";

export { acceptLanguage } from "./accept-language";

export { modifyHeaders } from "./modify-headers";
28 changes: 28 additions & 0 deletions packages/base/headers/src/modify-headers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { it, expect } from "vitest";
import { modifyHeaders } from "./modify-headers";

it("modifies mutable headers", () => {
const response = new Response("body", {
headers: { "Some-Header": "value" },
});

const modified = modifyHeaders(response, (headers) => {
headers.set("Some-Header", "modified");
headers.set("New-Header", "new");
});

expect(modified).toBe(response); // Should be in-place
expect(modified.headers.get("Some-Header")).toBe("modified");
expect(modified.headers.get("New-Header")).toBe("new");
});

it("modifies immutable headers", () => {
const response = Response.redirect("http://example.com");

const modified = modifyHeaders(response, (headers) => {
headers.set("Some-Header", "modified");
});

expect(modified).not.toBe(response); // Should not be in-place
expect(modified.headers.get("Some-Header")).toBe("modified");
});
43 changes: 43 additions & 0 deletions packages/base/headers/src/modify-headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Tries to modify the headers of a response, and, if it fails,
* creates a mutable copy of the response and tries again.
*
* The problem that this function solves is that some responses
* have immutable headers and there is no way to know this without
* trying to modify them. This function solves this problem by
* trying to modify the headers and, if it fails, creating a copy
* of the response with the same body and headers and trying again.
*
* Usage:
*
* ```ts
* app.use(async (ctx) => {
* let response = await ctx.next();
* response = modifyHeaders(response, (headers) => {
* headers.set("X-Powered-By", "Hattip");
* });
*
* return response;
* });
* ```
*
* @param response Response object to be modified
* @param modify Callback to modify the headers
*
* @returns a Response object with modified headers. It will be
* the same object as the argument if its headers are mutable,
* otherwise it will be a copy of the original.
*/
export function modifyHeaders(
response: Response,
modify: (headers: Headers) => void,
): Response {
try {
modify(response.headers);
return response;
} catch {
const clone = new Response(response.body, response);
modify(clone.headers);
return clone;
}
}
1 change: 1 addition & 0 deletions packages/middleware/cookie/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dependencies": {
"@hattip/compose": "workspace:*",
"@hattip/core": "workspace:*",
"@hattip/headers": "workspace:*",
"cookie": "^1.0.1"
},
"devDependencies": {
Expand Down
7 changes: 5 additions & 2 deletions packages/middleware/cookie/src/serialize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "@hattip/compose";
import type { RequestContext } from "@hattip/compose";
import { modifyHeaders } from "@hattip/headers";
import { serialize, SerializeOptions as CookieSerializeOptions } from "cookie";

declare module "@hattip/compose" {
Expand Down Expand Up @@ -74,10 +75,12 @@ export function cookieSerializer(defaultOptions?: CookieSerializeOptions) {
});
};

const response: Response = await ctx.next();
let response: Response = await ctx.next();

for (const { name, value, options } of ctx.outgoingCookies) {
response.headers.append("set-cookie", serialize(name, value, options));
response = modifyHeaders(response, (headers) => {
headers.append("set-cookie", serialize(name, value, options));
});
}

return response;
Expand Down
1 change: 1 addition & 0 deletions packages/middleware/cors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"dependencies": {
"@hattip/compose": "workspace:*",
"@hattip/core": "workspace:*",
"@hattip/headers": "workspace:*",
"@types/cookie": "^1.0.0",
"cookie": "^1.0.1"
},
Expand Down
16 changes: 12 additions & 4 deletions packages/middleware/cors/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RequestContext } from "@hattip/compose";
import { modifyHeaders } from "@hattip/headers";

/** CORS middleware */
export function cors(options: CorsOptions = {}) {
Expand Down Expand Up @@ -30,8 +31,11 @@ export function cors(options: CorsOptions = {}) {
const requestOrigin = ctx.request.headers.get("Origin");

if (!requestOrigin) {
const response = await ctx.next();
response.headers.append("Vary", "Origin");
let response = await ctx.next();
response = modifyHeaders(response, (headers) => {
headers.append("Vary", "Origin");
});

return response;
}

Expand All @@ -51,12 +55,16 @@ export function cors(options: CorsOptions = {}) {
credentials = !!options.credentials;
}

const response =
let response =
ctx.method === "OPTIONS"
? new Response(null, { status: 204 })
: await ctx.next();

response.headers.append("Vary", "Origin");
response = modifyHeaders(response, (headers) => {
headers.append("Vary", "Origin");
});

// From here on we can assume that the response has a mutable headers object

const headersSet: Record<string, string | string[]> = {};

Expand Down
Loading

0 comments on commit 5f4e686

Please sign in to comment.