Skip to content

Commit 8f37b6b

Browse files
committed
feat(middleware): filter conditional headers if selecting representation
1 parent d172600 commit 8f37b6b

File tree

7 files changed

+161
-10
lines changed

7 files changed

+161
-10
lines changed

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ For a definition of Universal HTTP middleware, see the
2121

2222
## Usage
2323

24-
To evaluate precondition, you need to provide a function to retrieve the
24+
To evaluate precondition, you need to provide
25+
[select representation](#select-representation) function to retrieve the
2526
[selected representation](https://www.rfc-editor.org/rfc/rfc9110#selected.representation).
2627

2728
The following example evaluates the `If-None-Match` precondition and handle
@@ -56,6 +57,32 @@ assertEquals(response.status, 304);
5657
assertFalse(response.body);
5758
```
5859

60+
## Select representation
61+
62+
The evaluation of the pre-conditions is always done on the selected
63+
representation.
64+
65+
You must provide a function to retrieve the representation.
66+
67+
Select representation is the following interface:
68+
69+
```ts
70+
interface SelectRepresentation {
71+
(request: Request): Response | Promise<Response>;
72+
}
73+
```
74+
75+
The select representation is executed prior to the handler when a request with a
76+
precondition header is received.
77+
78+
The select representation removes the pre-conditional header from the actual
79+
request header in order to satisfy the following requirement.
80+
81+
> A server MUST ignore all received preconditions if its response to the same
82+
> request without those conditions, prior to processing the request content,
83+
> would have been a status code other than a 2xx (Successful) or 412
84+
> (Precondition Failed).
85+
5986
## Precondition
6087

6188
[RFC 9110, 13.1. Preconditions](https://www.rfc-editor.org/rfc/rfc9110#section-13.1)

_dev_deps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export {
1212
spy,
1313
} from "https://deno.land/[email protected]/testing/mock.ts";
1414
export { equalsResponse } from "https://deno.land/x/[email protected]/response.ts";
15+
export { equalsRequest } from "https://deno.land/x/[email protected]/request.ts";
1516
export {
1617
ConditionalHeader,
1718
RangeHeader,

deps.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export {
66
Status,
77
type SuccessfulStatus,
88
} from "https://deno.land/[email protected]/http/http_status.ts";
9+
export { distinct } from "https://deno.land/[email protected]/collections/distinct.ts";
910
export {
1011
isBoolean,
1112
isNegativeNumber,
@@ -21,6 +22,7 @@ export {
2122
export {
2223
ConditionalHeader,
2324
filterKeys,
25+
isConditionalHeader,
2426
isRepresentationHeader,
2527
RangeHeader,
2628
RepresentationHeader,
@@ -36,11 +38,11 @@ export {
3638
} from "https://deno.land/x/[email protected]/mod.ts";
3739
export { isErr, unsafe } from "https://deno.land/x/[email protected]/mod.ts";
3840
export { ascend } from "https://deno.land/[email protected]/collections/_comparators.ts";
39-
export { withContentRange } from "https://deno.land/x/[email protected]-beta.1/transform.ts";
41+
export { withContentRange } from "https://deno.land/x/[email protected]/transform.ts";
4042
export {
4143
BytesRange,
4244
type Range,
43-
} from "https://deno.land/x/[email protected]-beta.1/mod.ts";
45+
} from "https://deno.land/x/[email protected]/mod.ts";
4446
export { default as parseHttpDate } from "https://esm.sh/[email protected]";
4547

4648
export function not<T extends readonly unknown[]>(

middleware.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ascendPrecondition,
99
isNotSelectionOrModificationMethod,
1010
isPreEvaluableStatus,
11+
withoutConditionHeaders,
1112
} from "./utils.ts";
1213
import { IfNoneMatch } from "./preconditions/if_none_match.ts";
1314
import { IfMatch } from "./preconditions/if_match.ts";
@@ -62,8 +63,9 @@ export function conditionalRequest(
6263
],
6364
).sort(ascendPrecondition);
6465

65-
return (request, next) =>
66-
_handler(selectRepresentation, preconditions, request, next);
66+
const curried = _handler.bind(null, selectRepresentation, preconditions);
67+
68+
return curried;
6769
}
6870

6971
/** Handle preconditions with all contexts.
@@ -88,7 +90,14 @@ export async function _handler(
8890

8991
if (!targetPreconditions.length) return next(request);
9092

91-
const selectedRepresentation = await selectRepresentation(request);
93+
const headers = withoutConditionHeaders(
94+
request.headers,
95+
preconditions.map(({ field }) => field),
96+
);
97+
98+
const selectedRepresentation = await selectRepresentation(
99+
new Request(request, { headers }),
100+
);
92101

93102
/** A server MUST ignore all received preconditions if its response to the same request without those conditions, prior to processing the request content, would have been a status code other than a 2xx (Successful) or 412 (Precondition Failed).
94103
*/

middleware_test.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
assertSpyCalls,
55
ConditionalHeader,
66
describe,
7+
equalsRequest,
78
equalsResponse,
89
it,
910
Method,
@@ -56,8 +57,12 @@ describe("_handler", () => {
5657
});
5758

5859
it("should call next handler if the selected response has not pre-evaluable status", async () => {
59-
const request = new Request("test:", { headers: { "test": "" } });
60-
const select = spy(() => new Response(null, { status: Status.NotFound }));
60+
const initRequest = new Request("test:", { headers: { "test": "" } });
61+
const select = spy(async (request: Request) => {
62+
assert(await equalsRequest(request, new Request("test:")));
63+
64+
return new Response(null, { status: Status.NotFound });
65+
});
6166
const initResponse = new Response();
6267
const next = spy(() => initResponse);
6368
const evaluate = spy(() => true);
@@ -66,14 +71,60 @@ describe("_handler", () => {
6671
const response = await _handler(
6772
select,
6873
[{ field: "test", evaluate, respond }],
69-
request,
74+
initRequest,
7075
next,
7176
);
7277

7378
assertSpyCalls(evaluate, 0);
7479
assertSpyCalls(respond, 0);
7580
assertSpyCalls(select, 1);
76-
assertSpyCallArg(select, 0, 0, request);
81+
assertSpyCalls(next, 1);
82+
assert(initResponse === response);
83+
});
84+
85+
it("should request what does not include precondition headers and custom precondition headers", async () => {
86+
const initRequest = new Request("test:", {
87+
headers: {
88+
[ConditionalHeader.IfNoneMatch]: "",
89+
[ConditionalHeader.IfMatch]: "",
90+
[ConditionalHeader.IfModifiedSince]: "",
91+
[ConditionalHeader.IfRange]: "",
92+
[ConditionalHeader.IfUnmodifiedSince]: "",
93+
"x-precondition": "",
94+
"x-test": "",
95+
},
96+
});
97+
const select = spy(async (request: Request) => {
98+
assert(
99+
await equalsRequest(
100+
request,
101+
new Request("test:", {
102+
headers: {
103+
"x-test": "",
104+
},
105+
}),
106+
),
107+
);
108+
109+
return new Response(null, { status: Status.NotFound });
110+
});
111+
const initResponse = new Response();
112+
const next = spy(() => initResponse);
113+
const evaluate = spy(() => true);
114+
const respond = spy(() => {});
115+
116+
const response = await _handler(
117+
select,
118+
[{ field: ConditionalHeader.IfNoneMatch, evaluate, respond }, {
119+
field: "X-Precondition",
120+
evaluate,
121+
respond,
122+
}],
123+
initRequest,
124+
next,
125+
);
126+
127+
assertSpyCalls(select, 1);
77128
assertSpyCalls(next, 1);
78129
assert(initResponse === response);
79130
});

utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33

44
import {
55
ascend,
6+
distinct,
7+
filterKeys,
68
isBoolean,
9+
isConditionalHeader,
710
isNegativeNumber,
811
isNull,
912
isSuccessfulStatus,
1013
Method,
14+
not,
1115
RepresentationHeader,
1216
Status,
1317
SuccessfulStatus,
@@ -89,3 +93,21 @@ export function isBannedHeader(fieldName: string): boolean {
8993
RepresentationHeader.ContentType,
9094
] as string[]).includes(fieldName);
9195
}
96+
97+
/** Return no precondition header. */
98+
export function withoutConditionHeaders(
99+
headers: Headers,
100+
additionalConditionHeaders: readonly string[] = [],
101+
): Headers {
102+
additionalConditionHeaders = distinct(additionalConditionHeaders)
103+
.map((v) => v.toLowerCase());
104+
105+
function isBannedHeader(key: string): boolean {
106+
return isConditionalHeader(key) ||
107+
additionalConditionHeaders.includes(key);
108+
}
109+
110+
const newHeaders = filterKeys(headers, not(isBannedHeader));
111+
112+
return newHeaders;
113+
}

utils_test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ascendPreconditionHeader,
44
type Ord,
55
toPriority,
6+
withoutConditionHeaders,
67
} from "./utils.ts";
78
import {
89
assert,
@@ -150,3 +151,41 @@ describe("applyPrecondition", () => {
150151
assert(newResponse === result);
151152
});
152153
});
154+
155+
describe("withoutConditionHeaders", () => {
156+
it("should return headers what does not include precondition header", () => {
157+
assertEquals(withoutConditionHeaders(new Headers()), new Headers());
158+
assertEquals(
159+
withoutConditionHeaders(
160+
new Headers({
161+
[ConditionalHeader.IfMatch]: "",
162+
[ConditionalHeader.IfModifiedSince]: "",
163+
[ConditionalHeader.IfNoneMatch]: "",
164+
[ConditionalHeader.IfRange]: "",
165+
[ConditionalHeader.IfUnmodifiedSince]: "",
166+
"x-x": "",
167+
}),
168+
),
169+
new Headers({ "x-x": "" }),
170+
);
171+
});
172+
173+
it("should add additional conditional headers", () => {
174+
assertEquals(
175+
withoutConditionHeaders(
176+
new Headers({
177+
[ConditionalHeader.IfMatch]: "",
178+
[ConditionalHeader.IfModifiedSince]: "",
179+
[ConditionalHeader.IfNoneMatch]: "",
180+
[ConditionalHeader.IfRange]: "",
181+
[ConditionalHeader.IfUnmodifiedSince]: "",
182+
"x-x": "",
183+
"x-test": "",
184+
"x-precondition": "",
185+
}),
186+
["x-precondition", "x-test", ConditionalHeader.IfMatch],
187+
),
188+
new Headers({ "x-x": "" }),
189+
);
190+
});
191+
});

0 commit comments

Comments
 (0)