Skip to content

Commit 4924476

Browse files
authored
Server parsimony (#372)
* refactor the HTTP.Server protocol to use fewer distinct code paths * expose @dynamicMemberLookup
1 parent 645bdbd commit 4924476

15 files changed

+253
-346
lines changed

Sources/HTTPServer/HTTP.Server.swift

Lines changed: 75 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,11 @@ extension HTTP
1717
public
1818
protocol Server:Sendable
1919
{
20-
associatedtype StreamedRequest:HTTP.ServerStreamedRequest
21-
2220
/// Checks whether the server should allow the request to proceed with an upload.
2321
/// Returns nil if the server should accept the upload, or an error response to send
24-
/// if the uploader lacks permissions.
25-
func clearance(for request:StreamedRequest) async throws -> HTTP.ServerResponse?
26-
27-
func response(for request:StreamedRequest,
28-
with body:__owned [UInt8]) async throws -> HTTP.ServerResponse
29-
30-
func get(
31-
request:ServerRequest,
32-
headers:HPACKHeaders) async throws -> HTTP.ServerResponse
33-
func get(
34-
request:ServerRequest,
35-
headers:HTTPHeaders) async throws -> HTTP.ServerResponse
36-
37-
func post(
38-
request:ServerRequest,
39-
headers:HPACKHeaders,
40-
body:[UInt8]) async throws -> HTTP.ServerResponse
41-
42-
func post(
43-
request:ServerRequest,
44-
headers:HTTPHeaders,
45-
body:[UInt8]) async throws -> HTTP.ServerResponse
22+
/// if the uploader lacks permissions. This is only called for `PUT` requests.
23+
func reject(request:ServerRequest) async throws -> ServerResponse?
24+
func accept(request:ServerRequest, method:ServerMethod) async throws -> ServerResponse
4625

4726
func log(event:ServerEvent, ip origin:ServerRequest.Origin?)
4827

@@ -51,33 +30,6 @@ extension HTTP
5130
}
5231
extension HTTP.Server
5332
{
54-
/// Inefficiently converts the headers to equivalent HPACK headers, and calls the witness
55-
/// for ``get(request:headers:) [4VK5G]``.
56-
///
57-
/// Servers that expect to handle a lot of HTTP/1.1 GET requests should override this with
58-
/// a more efficient implementation.
59-
@inlinable public
60-
func get(
61-
request:HTTP.ServerRequest,
62-
headers:HTTPHeaders) async throws -> HTTP.ServerResponse
63-
{
64-
try await self.get(request: request, headers: .init(httpHeaders: headers))
65-
}
66-
67-
/// Inefficiently converts the headers to equivalent HPACK headers, and calls the witness
68-
/// for ``post(request:headers:body:) [541MX]``.
69-
///
70-
/// Servers that expect to handle a lot of HTTP/1.1 POST requests should override this with
71-
/// a more efficient implementation.
72-
@inlinable public
73-
func post(
74-
request:HTTP.ServerRequest,
75-
headers:HTTPHeaders,
76-
body:[UInt8]) async throws -> HTTP.ServerResponse
77-
{
78-
try await self.post(request: request, headers: .init(httpHeaders: headers), body: body)
79-
}
80-
8133
/// Dumps detailed information about the caught error. This information will be shown to
8234
/// *anyone* accessing the server. In production, we strongly recommend overriding this
8335
/// default implementation to avoid inadvertently exposing sensitive data via type
@@ -401,17 +353,22 @@ extension HTTP.Server
401353
return .resource("Malformed URI\n", status: 400)
402354
}
403355

356+
let request:HTTP.ServerRequest = .init(headers: .http1_1(h1.headers),
357+
origin: origin,
358+
uri: uri)
359+
404360
switch h1.method
405361
{
406-
case .HEAD:
407-
fallthrough
362+
case .DELETE:
363+
return try await self.accept(request: request, method: .delete)
408364

409365
case .GET:
410-
return try await self.get(
411-
request: .init(origin: origin, uri: uri),
412-
headers: h1.headers)
366+
return try await self.accept(request: request, method: .get)
413367

414-
case .PUT:
368+
case .HEAD:
369+
return try await self.accept(request: request, method: .head)
370+
371+
case .POST:
415372
guard
416373
let length:String = h1.headers["content-length"].first,
417374
let length:Int = .init(length)
@@ -420,28 +377,21 @@ extension HTTP.Server
420377
return .resource("Content length required\n", status: 411)
421378
}
422379

423-
guard
424-
let request:StreamedRequest = .init(put: uri, headers: h1.headers)
425-
else
426-
{
427-
return .resource("Malformed request\n", status: 400)
428-
}
429-
430-
if let failure:HTTP.ServerResponse = try await self.clearance(for: request)
380+
if length > 1_000_000
431381
{
432-
return failure
382+
return .resource("Content too large\n", status: 413)
433383
}
434384

435385
guard
436386
let body:[UInt8] = try await inbound.accumulateBuffers(length: length)
437387
else
438388
{
439-
return .resource("Content length does not match payload\n", status: 413)
389+
return .resource("Content length does not match payload\n", status: 400)
440390
}
441391

442-
return try await self.response(for: request, with: body)
392+
return try await self.accept(request: request, method: .post(body))
443393

444-
case .POST:
394+
case .PUT:
445395
guard
446396
let length:String = h1.headers["content-length"].first,
447397
let length:Int = .init(length)
@@ -450,22 +400,19 @@ extension HTTP.Server
450400
return .resource("Content length required\n", status: 411)
451401
}
452402

453-
if length > 1_000_000
403+
if let failure:HTTP.ServerResponse = try await self.reject(request: request)
454404
{
455-
return .resource("Content too large\n", status: 413)
405+
return failure
456406
}
457407

458408
guard
459409
let body:[UInt8] = try await inbound.accumulateBuffers(length: length)
460410
else
461411
{
462-
return .resource("Content length does not match payload\n", status: 400)
412+
return .resource("Content length does not match payload\n", status: 413)
463413
}
464414

465-
return try await self.post(
466-
request: .init(origin: origin, uri: uri),
467-
headers: h1.headers,
468-
body: body)
415+
return try await self.accept(request: request, method: .put(body))
469416

470417
default:
471418
return .resource("Method requires HTTP/2\n", status: 505)
@@ -656,64 +603,20 @@ extension HTTP.Server
656603
return .resource("Malformed URI", status: 400)
657604
}
658605

606+
let request:HTTP.ServerRequest = .init(headers: .http2(headers),
607+
origin: origin,
608+
uri: path)
609+
659610
switch method
660611
{
661-
case "HEAD":
662-
// return .resource("Method not allowed", status: 405)
663-
fallthrough
612+
case "DELETE":
613+
return try await self.accept(request: request, method: .delete)
664614

665615
case "GET":
666-
return try await self.get(
667-
request: .init(origin: origin, uri: path),
668-
headers: headers)
669-
670-
case "PUT":
671-
guard
672-
let length:String = headers["content-length"].first,
673-
let length:Int = .init(length)
674-
else
675-
{
676-
return .resource("Content length required", status: 411)
677-
}
616+
return try await self.accept(request: request, method: .get)
678617

679-
guard
680-
let request:StreamedRequest = .init(put: path, headers: headers)
681-
else
682-
{
683-
return .resource("Malformed request", status: 400)
684-
}
685-
686-
if let failure:HTTP.ServerResponse = try await self.clearance(for: request)
687-
{
688-
return failure
689-
}
690-
691-
var body:[UInt8] = []
692-
body.reserveCapacity(length)
693-
694-
while let payload:HTTP2Frame.FramePayload? = try await inbound.next()
695-
{
696-
// We could care less about timeout events here, as we have already determined
697-
// the request originates from a trusted source.
698-
guard case .data(let payload)? = payload
699-
else
700-
{
701-
continue
702-
}
703-
704-
if case .byteBuffer(let payload) = payload.data
705-
{
706-
payload.withUnsafeReadableBytes { body += $0 }
707-
}
708-
709-
// Why can’t NIO do this for us?
710-
if payload.endStream
711-
{
712-
break
713-
}
714-
}
715-
716-
return try await self.response(for: request, with: body)
618+
case "HEAD":
619+
return try await self.accept(request: request, method: .head)
717620

718621
case "POST":
719622
guard
@@ -768,13 +671,51 @@ extension HTTP.Server
768671
}
769672
}
770673

771-
return try await self.post(
772-
request: .init(origin: origin, uri: path),
773-
headers: headers,
774-
body: body)
674+
return try await self.accept(request: request, method: .post(body))
675+
676+
case "PUT":
677+
guard
678+
let length:String = headers["content-length"].first,
679+
let length:Int = .init(length)
680+
else
681+
{
682+
return .resource("Content length required", status: 411)
683+
}
684+
685+
if let failure:HTTP.ServerResponse = try await self.reject(request: request)
686+
{
687+
return failure
688+
}
689+
690+
var body:[UInt8] = []
691+
body.reserveCapacity(length)
692+
693+
while let payload:HTTP2Frame.FramePayload? = try await inbound.next()
694+
{
695+
// We could care less about timeout events here, as we have already determined
696+
// the request originates from a trusted source.
697+
guard case .data(let payload)? = payload
698+
else
699+
{
700+
continue
701+
}
702+
703+
if case .byteBuffer(let payload) = payload.data
704+
{
705+
payload.withUnsafeReadableBytes { body += $0 }
706+
}
707+
708+
// Why can’t NIO do this for us?
709+
if payload.endStream
710+
{
711+
break
712+
}
713+
}
714+
715+
return try await self.accept(request: request, method: .put(body))
775716

776717
case _:
777-
return .forbidden("Forbidden")
718+
return .resource("Method not allowed\n", status: 405)
778719
}
779720
}
780721
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
extension HTTP
2+
{
3+
@frozen public
4+
enum ServerMethod:Sendable
5+
{
6+
case delete
7+
case get
8+
case head
9+
case post([UInt8])
10+
case put([UInt8])
11+
}
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import HTTP
2+
import NIOHPACK
3+
import NIOHTTP1
4+
5+
extension HTTP.ServerRequest
6+
{
7+
/// This type stores either ``HTTPHeaders`` or ``HPACKHeaders``, depending on the HTTP
8+
/// protocol version, as eagarly converting them to a common format would be wasteful.
9+
@frozen public
10+
enum Headers:Sendable
11+
{
12+
case http1_1(HTTPHeaders)
13+
case http2(HPACKHeaders)
14+
}
15+
}

Sources/HTTPServer/HTTP.ServerRequest.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@ import URI
33

44
extension HTTP
55
{
6-
/// A ``ServerRequest`` contains all the metadata about an incoming request, except for
7-
/// the headers. This is because the headers have a different format depending on the HTTP
8-
/// protocol version, and eagarly converting them to a common format would be wasteful.
6+
/// A ``ServerRequest`` contains all the metadata about an incoming request.
97
@frozen public
108
struct ServerRequest:Sendable
119
{
10+
public
11+
let headers:Headers
1212
public
1313
let origin:Origin
1414
public
1515
let uri:URI
1616

17-
init(origin:Origin, uri:URI)
17+
init(headers:Headers, origin:Origin, uri:URI)
1818
{
19+
self.headers = headers
1920
self.origin = origin
2021
self.uri = uri
2122
}

Sources/HTTPServer/HTTP.ServerStreamedRequest.swift

Lines changed: 0 additions & 25 deletions
This file was deleted.

Sources/UnidocServer/HTTP.Headers.swift

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)