Skip to content

Commit 3137f67

Browse files
committed
handle conditional requests before serving response
1 parent 535f57d commit 3137f67

File tree

3 files changed

+79
-4
lines changed

3 files changed

+79
-4
lines changed

src/Server.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import http from "node:http";
22
import packageJson from "../package.json" with {type: "json"};
33
import {Request} from "./Request.js";
4+
import {EmptyResponse} from "./response/index.js";
45
import {Response} from "./response/Response.js";
56
import {RouteRegistry} from "./routing/RouteRegistry.js";
67
import {ServerErrorRegistry} from "./ServerErrorRegistry.js";
@@ -16,6 +17,7 @@ class Server {
1617
public readonly routes = new RouteRegistry();
1718
private readonly server: http.Server;
1819
private readonly copyOrigin: boolean;
20+
private readonly handleConditionalRequests: boolean;
1921

2022
/**
2123
* This server's error registry.
@@ -36,6 +38,7 @@ class Server {
3638
this.globalHeaders.set("Server", `cldn/${packageJson.version}`);
3739

3840
this.copyOrigin = options.copyOrigin ?? false;
41+
this.handleConditionalRequests = options.handleConditionalRequests ?? false;
3942

4043
this.server.listen(options.port);
4144
}
@@ -80,7 +83,59 @@ class Server {
8083
response = this.errors._get(ServerErrorRegistry.ErrorCodes.INTERNAL, apiRequest);
8184
}
8285
}
83-
response._send(res, this, apiRequest);
86+
await this.sendResponse(response, res, apiRequest);
87+
}
88+
89+
private async sendResponse(response: Response, res: http.ServerResponse, req: Request): Promise<void> {
90+
conditional: if (
91+
this.handleConditionalRequests
92+
&& response.statusCode === 200
93+
&& [Request.Method.GET, Request.Method.HEAD].includes(req.method)
94+
) {
95+
const responseHeaders = response.allHeaders(res, this, req);
96+
const etag = responseHeaders.get("etag");
97+
const lastModified = responseHeaders.has("last-modified")
98+
? new Date(responseHeaders.get("last-modified")!)
99+
: null;
100+
if (etag === null && lastModified === null)
101+
break conditional;
102+
103+
if ((
104+
req.headers.has("if-match")
105+
&& !this.getETags(req.headers.get("if-match")!)
106+
.filter(t => !t.startsWith("W/"))
107+
.includes(etag!)
108+
)
109+
|| (
110+
req.headers.has("if-unmodified-since")
111+
&& (
112+
lastModified === null
113+
|| lastModified.getTime() > new Date(req.headers.get("if-unmodified-since")!).getTime()
114+
)
115+
)) return this.errors._get(ServerErrorRegistry.ErrorCodes.PRECONDITION_FAILED, req)._send(res, this, req);
116+
117+
if ((
118+
etag !== null
119+
&& (
120+
req.headers.has("if-none-match")
121+
&& this.getETags(req.headers.get("if-none-match")!).includes(etag!)
122+
)
123+
)
124+
|| (
125+
lastModified !== null
126+
&& (
127+
req.headers.has("if-modified-since")
128+
&& new Date(req.headers.get("if-modified-since")!).getTime() >= lastModified.getTime()
129+
)
130+
)) return new EmptyResponse(responseHeaders, 304)._send(res, this, req);
131+
}
132+
response._send(res, this, req);
133+
}
134+
135+
private getETags(header: string) {
136+
return header
137+
.split(",")
138+
.map(t => t.trim())
84139
}
85140

86141
public close(): Promise<void> {
@@ -119,6 +174,13 @@ namespace Server {
119174
* @default false
120175
*/
121176
readonly copyOrigin?: boolean;
177+
178+
/**
179+
* Automatically handle conditional requests for GET and HEAD requests that result in a 200 status code.
180+
* `If-Range` headers are ignored.
181+
* @default true
182+
*/
183+
readonly handleConditionalRequests?: boolean;
122184
}
123185
}
124186

src/ServerErrorRegistry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class ServerErrorRegistry {
2121

2222
[ServerErrorRegistry.ErrorCodes.INTERNAL]:
2323
new TextResponse("An internal error occurred.", 500),
24+
25+
[ServerErrorRegistry.ErrorCodes.PRECONDITION_FAILED]:
26+
new TextResponse("Precondition failed.", 412),
2427
};
2528
}
2629

@@ -49,6 +52,7 @@ namespace ServerErrorRegistry {
4952
BAD_URL,
5053
NO_ROUTE,
5154
INTERNAL,
55+
PRECONDITION_FAILED,
5256
}
5357
}
5458

src/response/Response.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export abstract class Response {
99
/**
1010
* The HTTP response status code to send.
1111
*/
12-
protected readonly statusCode: number;
12+
public readonly statusCode: number;
1313

1414
/**
1515
* The HTTP response headers to send.
@@ -34,9 +34,10 @@ export abstract class Response {
3434
}
3535

3636
/**
37-
* Set the HTTP response status code and headers.
37+
* All (final) headers to send to the client.
38+
* @internal
3839
*/
39-
protected writeHead(res: http.ServerResponse, server: Server, req?: Request) {
40+
public allHeaders(res: http.ServerResponse, server: Server, req?: Request) {
4041
const headers = new Headers(this.headers);
4142
if (req !== undefined)
4243
for (const [key, value] of req._responseHeaders)
@@ -53,6 +54,14 @@ export abstract class Response {
5354
headers.set("connection", "keep-alive");
5455
headers.set("keep-alive", "timeout=" + server._keepAliveTimeout);
5556
}
57+
return headers;
58+
}
59+
60+
/**
61+
* Set the HTTP response status code and headers.
62+
*/
63+
protected writeHead(res: http.ServerResponse, server: Server, req?: Request) {
64+
const headers = this.allHeaders(res, server, req);
5665
for (const [key, value] of Array.from(headers.entries())
5766
.sort((a, b) => a[0].localeCompare(b[0])))
5867
res.setHeader(key, value);

0 commit comments

Comments
 (0)