[swift-server-dev] HTTP API Sketch v2
Johannes Weiß
johannesweiss at apple.com
Fri Apr 7 05:01:26 CDT 2017
Hi Colin,
> On 6 Apr 2017, at 22:20, Colin Barrett <colin at springsandstruts.com> wrote:
>
> On Thu, Apr 6, 2017 at 3:53 AM Johannes Weiß <johannesweiss at apple.com> wrote:
> Hi Colin,
>
>> Is writing compositional middleware a goal for this iteration of the API? I noticed that the HTTPResponseWriter doesn't appear to allow for, e.g. writing a single header and then passing the writer on (as one would do in, say, an anti-CSRF middleware). Possible I'm missing something though!
>
> Good point! The version 1 actually only had writeHeader(key:value:) and for the sake of having a HTTPResponse type (that could then be used by an HTTP client) I got rid of that. But maybe that was over the top and we should go back to what I had before.
>
> I think having the HTTPResponse is nice. I would suggest these changes:
>
> writeResponse(..., finishHeaders = true)
> writeHeader(...)
> finishHeaders() [-> Bool] // It is an error to call writeHeader after finishHeaders. Calling finishHeaders twice is a no-op. (Return value could differentiates those cases?)
> writeBody(...) // Calls finishHeaders()
>
> Sorry for the brevity of my sketch, let me know if that's unclear in any way.
Thanks, that makes sense! I literally just started an API Sketch v3 document where I put these in so I won't forget.
Just one question about the finishHeaders(): You see this as being mandatory to call, right? At the moment our implementation doesn't have that and it's handled implicitly. Basically if writeBody/writeTrailer/done are called we finish the headers if that hasn't been done yet. In other words, there's a state machine in HTTPResponseWriter which has a headers finished state but it's managed internally.
Do you see any advantages of exposing that explicitly?
Cheers,
Johannes
>
> Thanks,
> -Colin
>
>
> --
> Johannes
>
>>
>> -Colin
>>
>> On Wed, Apr 5, 2017 at 1:36 PM Johannes Weiß via swift-server-dev <swift-server-dev at swift.org> 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 ---
>>
>> _______________________________________________
>> swift-server-dev mailing list
>> swift-server-dev at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-server-dev
More information about the swift-server-dev
mailing list