11import http from "node:http" ;
22import packageJson from "../package.json" with { type : "json" } ;
33import { Request } from "./Request.js" ;
4+ import { EmptyResponse } from "./response/index.js" ;
45import { Response } from "./response/Response.js" ;
56import { RouteRegistry } from "./routing/RouteRegistry.js" ;
67import { 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
0 commit comments