[swift-server-dev] HTTP API Sketch v2
Helge Heß
me at helgehess.eu
Thu Apr 6 03:28:39 CDT 2017
Hi Johannes,
my notes on it:
- concrete types instead of protocols for HTTPRequest
- bad for layering this on top of alternative
implementations
- bad because it prohibits future changes in the
implementation (e.g. I still think it is a bad idea
to convert everything to Strings in advance)
- it can still come with a default imp
- also: you could make NSURLRequest/Response support it
- maybe HTTPRequest should be called HTTPRequestHead,
because in this case it really is what it is, it
doesn’t even have a stream reference attached to it.
- you should probably not add ‘convenience’ to this, that
will be done by frameworks using it in whatever they
consider ‘convenient’
- or it other words: does providing a `write` w/o a
completion handler give performance benefits and
is part of the API because of this, or is it just
`write(..) { _ in }`. I could see that it can be a
perf advantage (no setup of an escaping closure
necessary), but then it should not be marked as
‘convenience’
- same for Data vs DispatchData. Decide on just one?
- maybe such:
func writeTrailer(key: String, value: String)
should be
func writeTrailer(key: UnsafePointer<CChar>,
value: UnsafePointer<CChar>)
this also works with strings out of the box while
allowing applications not using Strings for protocol
data ;->
- I guess all ‘write’ functions need a completion
callback? (including writeTrailer, writeResponse)
- I’m still a little worried about using `enum`s for
an evolving protocol. That will look nice now but
will fall apart in the future as we can’t extend
them w/o breaking apps
- Talking enums. You use them for status and method,
why not for header keys? I see _big_ performance
advantages in that.
- Having the stream support ‘HTTPResponse’ looks like
too high level for your specific design. You can keep
that object and add a convenience method to put such
on the stream.
- also: HTTPResponse vs HTTPResponseHead[er?]
That is it for now :-)
hh
> On 5. Apr 2017, at 19:35, Johannes Weiß <johannesweiss at apple.com> wrote:
>
> Hi,
>
> First of all, thanks very much to Helge for carefully analysing (and implementing) the first API sketch. As promised, I reworked it a bit.
>
> Changes:
> - extracted HTTPResponse as its own value type (so it could also be used for a client)
> - added Helge's suggestions:
> * proper backpressure support
> * being able to ignore further parts of the HTTP body (stop parameter)
>
> If I forgot something important, please let me know. I can't promise that I'll address it before the meeting as I'm in another office tomorrow for meetings and probably won't be able to attend either.
>
> Please find everything below...
>
> Cheers,
> Johannes
>
> --- SNIP ---
> /* a web app is a function that gets a HTTPRequest and a HTTPResponseWriter and returns a function which processes the HTTP request body in chunks as they arrive */
>
> public typealias WebApp = (HTTPRequest, HTTPResponseWriter) -> HTTPBodyProcessing
>
> public struct HTTPRequest {
> public var method : HTTPMethod
> public var target : String /* e.g. "/foo/bar?buz=qux" */
> public var httpVersion : HTTPVersion
> public var headers : HTTPHeaders
> }
>
> public struct HTTPResponse {
> public var httpVersion : HTTPVersion
> public var status: HTTPResponseStatus
> public var transferEncoding: HTTPTransferEncoding
> public var headers: HTTPHeaders
> }
>
> public protocol HTTPResponseWriter: class {
> func writeContinue(headers: HTTPHeaders = HTTPHeaders()) /* to send an HTTP `100 Continue` */
>
> func writeResponse(_ response: HTTPResponse)
>
> func writeTrailer(key: String, value: String)
>
> func writeBody(data: DispatchData) /* convenience */
> func writeBody(data: Data) /* convenience */
> func writeBody(data: DispatchData, completion: @escaping (Result<POSIXError, ()>) -> Void)
> func writeBody(data: Data, completion: @escaping (Result<POSIXError, ()>) -> Void)
>
> func done() /* convenience */
> func done(completion: @escaping (Result<POSIXError, ()>) -> Void)
> func abort()
> }
>
> public typealias HTTPBodyHandler = (HTTPBodyChunk, inout Bool) -> Void /* the Bool can be set to true when we don't want to process anything further */
>
> public enum HTTPBodyProcessing {
> case discardBody /* if you're not interested in the body */
> case processBody(handler: HTTPBodyHandler)
> }
>
> public enum HTTPBodyChunk {
> case chunk(data: DispatchData, finishedProcessing: () -> Void) /* a new bit of the HTTP request body has arrived, finishedProcessing() must be called when done with that chunk */
> case failed(error: HTTPParserError) /* error while streaming the HTTP request body, eg. connection closed */
> case trailer(key: String, value: String) /* trailer has arrived (this we actually haven't implemented yet) */
> case end /* body and trailers finished */
> }
>
> public struct HTTPHeaders {
> var storage: [String:[String]] /* lower cased keys */
> var original: [(String, String)] /* original casing */
> var description: String
>
> subscript(key: String) -> [String]
> func makeIterator() -> IndexingIterator<Array<(String, String)>>
>
> init(_ headers: [(String, String)] = [])
> }
>
> public typealias HTTPVersion = (Int, Int)
>
> public enum HTTPTransferEncoding {
> case identity(contentLength: UInt)
> case chunked
> }
>
> public enum HTTPResponseStatus {
> /* use custom if you want to use a non-standard response code or
> have it available in a (UInt, String) pair from a higher-level web framework. */
> case custom(code: UInt, reasonPhrase: String)
>
> /* all the codes from http://www.iana.org/assignments/http-status-codes */
> case `continue`
> case switchingProtocols
> case processing
> case ok
> case created
> case accepted
> case nonAuthoritativeInformation
> case noContent
> case resetContent
> case partialContent
> case multiStatus
> case alreadyReported
> case imUsed
> case multipleChoices
> case movedPermanently
> case found
> case seeOther
> case notModified
> case useProxy
> case temporaryRedirect
> case permanentRedirect
> case badRequest
> case unauthorized
> case paymentRequired
> case forbidden
> case notFound
> case methodNotAllowed
> case notAcceptable
> case proxyAuthenticationRequired
> case requestTimeout
> case conflict
> case gone
> case lengthRequired
> case preconditionFailed
> case payloadTooLarge
> case uriTooLong
> case unsupportedMediaType
> case rangeNotSatisfiable
> case expectationFailed
> case misdirectedRequest
> case unprocessableEntity
> case locked
> case failedDependency
> case upgradeRequired
> case preconditionRequired
> case tooManyRequests
> case requestHeaderFieldsTooLarge
> case unavailableForLegalReasons
> case internalServerError
> case notImplemented
> case badGateway
> case serviceUnavailable
> case gatewayTimeout
> case httpVersionNotSupported
> case variantAlsoNegotiates
> case insufficientStorage
> case loopDetected
> case notExtended
> case networkAuthenticationRequired
> }
>
> public enum HTTPMethod {
> case custom(method: String)
>
> /* everything that http_parser.[ch] supports */
> case DELETE
> case GET
> case HEAD
> case POST
> case PUT
> case CONNECT
> case OPTIONS
> case TRACE
> case COPY
> case LOCK
> case MKCOL
> case MOVE
> case PROPFIND
> case PROPPATCH
> case SEARCH
> case UNLOCK
> case BIND
> case REBIND
> case UNBIND
> case ACL
> case REPORT
> case MKACTIVITY
> case CHECKOUT
> case MERGE
> case MSEARCH
> case NOTIFY
> case SUBSCRIBE
> case UNSUBSCRIBE
> case PATCH
> case PURGE
> case MKCALENDAR
> case LINK
> case UNLINK
> }
> --- SNAP ---
>
> Here's the demo code for a simple echo server
>
> --- SNIP ---
> serve { (req, res) in
> if req.target == "/echo" {
> guard req.httpVersion == (1, 1) else {
> /* HTTP/1.0 doesn't support chunked encoding */
> res.writeResponse(HTTPResponse(version: req.version,
> status: .httpVersionNotSupported,
> transferEncoding: .identity(contentLength: 0)))
> res.done()
> return .discardBody
> }
> res.writeResponse(HTTPResponse(version: req.version,
> status: .ok,
> transferEncoding: .chunked,
> headers: SomeConcreteHTTPHeaders([("X-foo": "bar")])))
> return .processBody { (chunk, stop) in
> switch chunk {
> case .chunk(let data, let finishedProcessing):
> res.writeBody(data: data) { _ in
> finishedProcessing()
> }
> case .end:
> res.done()
> default:
> stop = true /* don't call us anymore */
> res.abort()
> }
> }
> } else { ... }
> }
> --- SNAP ---
>
More information about the swift-server-dev
mailing list