Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for sending Trailers #566

Closed
cstrahan opened this Issue Jul 7, 2016 · 7 comments

Comments

Projects
None yet
3 participants
@cstrahan
Copy link

cstrahan commented Jul 7, 2016

wai/warp ought to support sending and receiving Trailers. I'd like to implement a gRPC library for Haskell, and the protocol requires support for reading and sending trailers. In Go, this might look something like (from the docs):

package main

import (
    "io"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/sendstrailers", func(w http.ResponseWriter, req *http.Request) {
        // Before any call to WriteHeader or Write, declare
        // the trailers you will set during the HTTP
        // response. These three headers are actually sent in
        // the trailer.
        w.Header().Set("Trailer", "AtEnd1, AtEnd2")
        w.Header().Add("Trailer", "AtEnd3")

        w.Header().Set("Content-Type", "text/plain; charset=utf-8") // normal header
        w.WriteHeader(http.StatusOK)

        w.Header().Set("AtEnd1", "value 1")
        io.WriteString(w, "This HTTP response has both headers before this text and trailers at the end.\n")
        w.Header().Set("AtEnd2", "value 2")
        w.Header().Set("AtEnd3", "value 3") // These will appear as trailers.
    })
}

There's also a TrailerPrefix constant:

const TrailerPrefix = "Trailer:"

TrailerPrefix is a magic prefix for ResponseWriter.Header map keys that, if present, signals that the map entry is actually for the response trailers, and not the response headers. The prefix is stripped after the ServeHTTP call finishes and the values are sent in the trailers.

This mechanism is intended only for trailers that are not known prior to the headers being written. If the set of trailers is fixed or known before the header is written, the normal Go trailers mechanism is preferred.

@cstrahan cstrahan referenced this issue Jul 7, 2016

Closed

Trailer support #11

@kazu-yamamoto

This comment has been minimized.

Copy link
Contributor

kazu-yamamoto commented Jul 8, 2016

Are you planning to implement gRPC by streaming?
If so, is it possible to decide the contents of trailers before pushing the final chunk of the streaming?

@awpr

This comment has been minimized.

Copy link
Member

awpr commented Jul 8, 2016

This is the same use case I intended for trailers support on #402 -- gRPC requires trailers to include the RPC status code, so they're not determined until the RPC is done (imagine an I/O error on the last chunk of a ReadFile RPC). What's worse, failed requests are required to be 200s with an error code in the trailers -- any partial or complete response without trailers is a protocol error. So, to be a conforming server, it needs to be able to set trailers even if the RPC is killed by an exception. That's why my design put the Trailers in the result of the stream body, so that it could use 'try' or something similar to convert exceptions into Trailers.

@kazu-yamamoto kazu-yamamoto added the http2 label Jul 12, 2016

@kazu-yamamoto kazu-yamamoto self-assigned this Jul 12, 2016

@kazu-yamamoto

This comment has been minimized.

Copy link
Contributor

kazu-yamamoto commented Jul 14, 2016

I have implemented APIs for trailers. Please try the trailers branch.
An application should set trailers just before exiting:

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Control.Exception
import Network.HTTP.Types (status200)
import Network.Wai
import Network.Wai.Handler.Warp
import Control.Concurrent
import Data.ByteString.Builder (byteString)

main :: IO ()
main = runInUnboundThread $ runSettings settings app
  where
    settings = setHost "127.0.0.1"
             $ setPort 3000
             $ setFileInfoCacheDuration 10
             $ setFdCacheDuration 10
               defaultSettings

app :: Application
app req respond = bracket_
    (putStrLn "Allocating scarce resource")
    (putStrLn "Cleaning up")
    $ respond $ responseStream status200 [] $ \write flush -> do
        write $ byteString "Hello\n"
        flush
        write $ byteString "World\n"
        modifyHTTP2Data req func
  where
    func Nothing    = Just $! defaultHTTP2Data { http2dataTrailers = [("x-status","OK")] }
    func (Just h2d) = Just $! h2d { http2dataTrailers = [("x-status","OK")] }
@cstrahan

This comment has been minimized.

Copy link
Author

cstrahan commented Jul 17, 2016

@kazu-yamamoto Thanks for implementing this! I'm afraid I'm a bit too tied up to be able to give this a solid review, but I think it looks like what I'd need.

@awpr Perhaps you could weigh in here? I figure you might have given more thought to gRPC than I have, in which case you might have an easier time spotting any problems this might pose for an implementation thereof.

@awpr

This comment has been minimized.

Copy link
Member

awpr commented Jul 18, 2016

Looks about as suitable for gRPC as the previous incarnation. As long as there can be exceptions in the Warp code that aren't catchable by the stream body, there's still the possibility for protocol errors in pathological cases, but this should make it possible to get it right for normal situations. I'm not too enthusiastic about supplying trailers by modifying an IORef in a Vault, but the alternatives all involve some form of compatibility break, so this seems like a reasonable workaround.

@kazu-yamamoto

This comment has been minimized.

Copy link
Contributor

kazu-yamamoto commented Jul 19, 2016

@awpr Thank you for your review. I would like to strengthen the support of error handling on feedback base. To let users be able to use this API, I would like to merge this first.

kazu-yamamoto added a commit that referenced this issue Jul 19, 2016

@kazu-yamamoto

This comment has been minimized.

Copy link
Contributor

kazu-yamamoto commented Jul 19, 2016

Merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.