-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(middleware): add etag middleware factory
- Loading branch information
1 parent
7ac8005
commit 0c26565
Showing
10 changed files
with
319 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export { | ||
assert, | ||
assertEquals, | ||
assertThrows, | ||
} from "https://deno.land/std@0.180.0/testing/asserts.ts"; | ||
export { describe, it } from "https://deno.land/std@0.180.0/testing/bdd.ts"; | ||
export { | ||
assertSpyCalls, | ||
spy, | ||
} from "https://deno.land/std@0.180.0/testing/mock.ts"; | ||
export { equalsResponse } from "https://deno.land/x/http_utils@1.0.0-beta.13/response.ts"; | ||
export { RepresentationHeader } from "https://deno.land/x/http_utils@1.0.0-beta.13/header.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
export { toHashString } from "https://deno.land/std@0.180.0/crypto/to_hash_string.ts"; | ||
export { isString } from "https://deno.land/x/isx@1.0.0-beta.24/mod.ts"; | ||
export { | ||
type Handler, | ||
type Middleware, | ||
} from "https://deno.land/x/http_middleware@1.0.0/mod.ts"; | ||
export { | ||
RepresentationHeader, | ||
} from "https://deno.land/x/http_utils@1.0.0-beta.13/header.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
import { type Middleware } from "./deps.ts"; | ||
import { withEtag } from "./transform.ts"; | ||
import { weakETag } from "./utils.ts"; | ||
import { CalculateETag } from "./types.ts"; | ||
|
||
/** Create `ETag` header middleware. | ||
* | ||
* @example | ||
* ```ts | ||
* import { etag } from "https://deno.land/x/http_etag@$VERSION/mod.ts"; | ||
* import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; | ||
* | ||
* const middleware = etag(); | ||
* const response = await middleware( | ||
* new Request("http://localhost"), | ||
* (request) => new Response("ok"), | ||
* ); | ||
* | ||
* assertEquals(response.headers.get("etag"), `"<body:SHA-1>"`); | ||
* ``` | ||
*/ | ||
export function etag(calculateETag?: CalculateETag): Middleware { | ||
const calculate = calculateETag ?? weakETag; | ||
|
||
return async (request, next) => { | ||
const response = await next(request); | ||
|
||
return withEtag(response, calculate); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
export { type Handler, type Middleware } from "./deps.ts"; | ||
export { etag } from "./middleware.ts"; | ||
export type { CalculateETag, ETag } from "./types.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
import { isString, RepresentationHeader } from "./deps.ts"; | ||
import { stringify } from "./etag.ts"; | ||
import type { CalculateETag } from "./types.ts"; | ||
|
||
/** | ||
* @param response Response | ||
* @param calculate Function to calculate hash values. The data is passed the actual response body value. | ||
* @returns | ||
*/ | ||
export async function withEtag( | ||
response: Response, | ||
calculate: CalculateETag, | ||
): Promise<Response> { | ||
if ( | ||
!response.ok || | ||
!response.body || | ||
response.bodyUsed || | ||
response.headers.has(RepresentationHeader.ETag) | ||
) return response; | ||
|
||
const etagLike = await calculate(response.clone()); | ||
const etag = isString(etagLike) ? etagLike : stringify(etagLike); | ||
const res = response.clone(); | ||
|
||
res.headers.set(RepresentationHeader.ETag, etag); | ||
|
||
return res; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { | ||
assert, | ||
assertEquals, | ||
assertSpyCalls, | ||
describe, | ||
equalsResponse, | ||
it, | ||
RepresentationHeader, | ||
spy, | ||
} from "./_dev_deps.ts"; | ||
import { withEtag } from "./transform.ts"; | ||
|
||
describe("withEtag", () => { | ||
it("should return with etag header field if the response does not have etag field", async () => { | ||
const digest = spy(() => `""`); | ||
const initResponse = new Response("ok"); | ||
const response = await withEtag(initResponse, digest); | ||
|
||
assertSpyCalls(digest, 1); | ||
assert( | ||
await equalsResponse( | ||
response, | ||
new Response("ok", { headers: { [RepresentationHeader.ETag]: `""` } }), | ||
true, | ||
), | ||
); | ||
}); | ||
|
||
it("should process custom digest what return strong etag object", async () => { | ||
const digest = spy(() => ({ weak: false, tag: "abc" })); | ||
const initResponse = new Response("ok"); | ||
const response = await withEtag(initResponse, digest); | ||
|
||
assertSpyCalls(digest, 1); | ||
assert( | ||
await equalsResponse( | ||
response, | ||
new Response("ok", { | ||
headers: { | ||
[RepresentationHeader.ETag]: `"abc"`, | ||
}, | ||
}), | ||
true, | ||
), | ||
); | ||
}); | ||
|
||
it("should process custom digest what return weak etag object", async () => { | ||
const digest = spy(() => ({ weak: true, tag: "abc" })); | ||
const initResponse = new Response("ok"); | ||
const response = await withEtag(initResponse, digest); | ||
|
||
assertSpyCalls(digest, 1); | ||
assert( | ||
await equalsResponse( | ||
response, | ||
new Response("ok", { | ||
headers: { | ||
[RepresentationHeader.ETag]: `W/"abc"`, | ||
}, | ||
}), | ||
true, | ||
), | ||
); | ||
}); | ||
|
||
it("should return same response if the response status code is not 2xx", async () => { | ||
const initResponse = new Response("", { status: 404 }); | ||
const digest = spy(() => ""); | ||
|
||
const response = await withEtag(initResponse, digest); | ||
|
||
assertSpyCalls(digest, 0); | ||
assertEquals(initResponse, response); | ||
}); | ||
|
||
it("should return same response if the response body does not exist", async () => { | ||
const initResponse = new Response(); | ||
const digest = spy(() => ""); | ||
|
||
const response = await withEtag(initResponse, digest); | ||
|
||
assertSpyCalls(digest, 0); | ||
assertEquals(initResponse, response); | ||
}); | ||
|
||
it("should return same response if the response has been read", async () => { | ||
const initResponse = new Response("ok"); | ||
const digest = spy(() => ""); | ||
|
||
await initResponse.text(); | ||
const response = await withEtag(initResponse, digest); | ||
|
||
assert(initResponse.bodyUsed); | ||
assertSpyCalls(digest, 0); | ||
assertEquals(initResponse, response); | ||
}); | ||
|
||
it("should return same response if the response has etag header", async () => { | ||
const initResponse = new Response("ok", { | ||
headers: { [RepresentationHeader.ETag]: "" }, | ||
}); | ||
const digest = spy(() => ""); | ||
|
||
const response = await withEtag(initResponse, digest); | ||
|
||
assertSpyCalls(digest, 0); | ||
assertEquals(initResponse, response); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
/** Function to calculate `<entity-tag>`. */ | ||
export interface CalculateETag { | ||
(response: Response): string | ETag | Promise<string | ETag>; | ||
} | ||
|
||
export interface ETag { | ||
/** Whether this is weak etag or not. */ | ||
readonly weak: boolean; | ||
|
||
/** Representation of [`<etagc>`](https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.3-2). */ | ||
readonly tag: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
import { toHashString } from "./deps.ts"; | ||
|
||
/** Catch error utility. */ | ||
export function reason( | ||
message: string, | ||
error: ErrorConstructor = Error, | ||
): (cause: unknown) => never { | ||
return (cause) => { | ||
throw error(message, { cause }); | ||
}; | ||
} | ||
|
||
/** Return quoted string. */ | ||
export function quoted<T extends string>(input: T): `"${T}"`; | ||
export function quoted(input: string): string; | ||
export function quoted(input: string): string { | ||
return `"${input}"`; | ||
} | ||
|
||
export function weakPrefix<T extends string>(input: T): `W/${T}`; | ||
export function weakPrefix(input: string): `W/${string}`; | ||
export function weakPrefix(input: string): `W/${string}` { | ||
return `W/${input}`; | ||
} | ||
|
||
export function weakETag(response: Response): Promise<string> { | ||
return response | ||
.clone() | ||
.arrayBuffer() | ||
.catch(reason(FailBy.Fetch)) | ||
.then(digestSha1) | ||
.catch(reason(FailBy.CalcHash)) | ||
.then(toHashString) | ||
.catch(reason(FailBy.CalcHashString)) | ||
.then(quoted) | ||
.then(weakPrefix); | ||
} | ||
|
||
export function digestSha1(data: ArrayBuffer): Promise<ArrayBuffer> { | ||
return crypto.subtle.digest("sha-1", data); | ||
} | ||
|
||
const enum FailBy { | ||
Fetch = "fail to fetch resource", | ||
CalcHash = "fail to calculate hash", | ||
CalcHashString = "fail to calculate hash string", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { quoted, reason, weakETag, weakPrefix } from "./utils.ts"; | ||
import { | ||
assert, | ||
assertEquals, | ||
assertThrows, | ||
describe, | ||
it, | ||
} from "./_dev_deps.ts"; | ||
|
||
describe("reason", () => { | ||
it("should throw error", () => { | ||
assertThrows(() => reason("test")(Error())); | ||
}); | ||
}); | ||
|
||
describe("weakPrefix", () => { | ||
it("should return string", () => { | ||
assertEquals(weakPrefix(""), "W/"); | ||
assertEquals(weakPrefix("a"), "W/a"); | ||
}); | ||
}); | ||
|
||
describe("quoted", () => { | ||
it("should return quoted string", () => { | ||
assertEquals(quoted(""), `""`); | ||
assertEquals(quoted("a"), `"a"`); | ||
}); | ||
}); | ||
|
||
describe("weakTag", () => { | ||
it("should return weak etag", async () => { | ||
const response = new Response(""); | ||
assertEquals( | ||
await weakETag(response), | ||
`W/"da39a3ee5e6b4b0d3255bfef95601890afd80709"`, | ||
); | ||
assert(!response.bodyUsed); | ||
}); | ||
|
||
it("should throw error if the response has been read", async () => { | ||
const response = new Response(""); | ||
|
||
await response.text(); | ||
|
||
assert(response.bodyUsed); | ||
assertThrows(() => weakETag(response)); | ||
}); | ||
}); |