mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
* fix up response headers
* stops defaulting to application/json. this was something awful, go stdlib has
a func to detect content type. sadly, it doesn't contain json, but we can do a
pretty good job by checking for an opening '{'... there are other fish in the
sea, and now we handle them nicely instead of saying it's a json [when it's
not]. a test confirms this, there should be no breakage for any routes
returning a json blob that were relying on us defaulting to this format
(granted that they start with a '{').
* buffers output now to a buffer for all protocol types (default is no longer
left out in the cold). use a little response writer so that we can still let
users write headers from their functions. this is useful for content type
detection instead of having to do it in multiple places.
* plumbs the little content type bit into fn-test-util just so we can test it,
we don't want to put this in the fdk since it's redundant.
I am totally in favor of getting rid of content type from the top level json
blurb. it's redundant, at best, and can have confusing behaviors if a user
uses both the headers and the content_type field (we override with the latter,
now). it's client protocol specific to http to a certain degree, other
protocols may use this concept but have their own way to set it (like http
does in headers..). I realize that it mostly exists because it's somewhat gross
to have to index a list from the headers in certain languages more than
others, but with the ^ behavior, is it really worth it?
closes #782
* reset idle timeouts back
* move json prefix to stack / next to use
145 lines
4.1 KiB
Go
145 lines
4.1 KiB
Go
package protocol
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/fnproject/fn/api/models"
|
|
opentracing "github.com/opentracing/opentracing-go"
|
|
)
|
|
|
|
var (
|
|
bufPool = &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}
|
|
)
|
|
|
|
// CallRequestHTTP for the protocol that was used by the end user to call this function. We only have HTTP right now.
|
|
type CallRequestHTTP struct {
|
|
Type string `json:"type"`
|
|
Method string `json:"method"`
|
|
RequestURL string `json:"request_url"`
|
|
Headers http.Header `json:"headers"`
|
|
}
|
|
|
|
// CallResponseHTTP for the protocol that was used by the end user to call this function. We only have HTTP right now.
|
|
type CallResponseHTTP struct {
|
|
StatusCode int `json:"status_code,omitempty"`
|
|
Headers http.Header `json:"headers,omitempty"`
|
|
}
|
|
|
|
// jsonIn We're not using this since we're writing JSON directly right now, but trying to keep it current anyways, much easier to read/follow
|
|
type jsonIn struct {
|
|
CallID string `json:"call_id"`
|
|
Deadline string `json:"deadline"`
|
|
Body string `json:"body"`
|
|
ContentType string `json:"content_type"`
|
|
Protocol CallRequestHTTP `json:"protocol"`
|
|
}
|
|
|
|
// jsonOut the expected response from the function container
|
|
type jsonOut struct {
|
|
Body string `json:"body"`
|
|
ContentType string `json:"content_type"`
|
|
Protocol *CallResponseHTTP `json:"protocol,omitempty"`
|
|
}
|
|
|
|
// JSONProtocol converts stdin/stdout streams from HTTP into JSON format.
|
|
type JSONProtocol struct {
|
|
// These are the container input streams, not the input from the request or the output for the response
|
|
in io.Writer
|
|
out io.Reader
|
|
}
|
|
|
|
func (p *JSONProtocol) IsStreamable() bool {
|
|
return true
|
|
}
|
|
|
|
func (h *JSONProtocol) writeJSONToContainer(ci CallInfo) error {
|
|
buf := bufPool.Get().(*bytes.Buffer)
|
|
buf.Reset()
|
|
defer bufPool.Put(buf)
|
|
|
|
_, err := io.Copy(buf, ci.Input())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
body := buf.String()
|
|
|
|
in := jsonIn{
|
|
Body: body,
|
|
ContentType: ci.ContentType(),
|
|
CallID: ci.CallID(),
|
|
Deadline: ci.Deadline().String(),
|
|
Protocol: CallRequestHTTP{
|
|
Type: ci.ProtocolType(),
|
|
Method: ci.Method(),
|
|
RequestURL: ci.RequestURL(),
|
|
Headers: ci.Headers(),
|
|
},
|
|
}
|
|
|
|
return json.NewEncoder(h.in).Encode(in)
|
|
}
|
|
|
|
func (h *JSONProtocol) Dispatch(ctx context.Context, ci CallInfo, w io.Writer) error {
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "dispatch_json")
|
|
defer span.Finish()
|
|
|
|
span, _ = opentracing.StartSpanFromContext(ctx, "dispatch_json_write_request")
|
|
err := h.writeJSONToContainer(ci)
|
|
span.Finish()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
span, _ = opentracing.StartSpanFromContext(ctx, "dispatch_json_read_response")
|
|
var jout jsonOut
|
|
err = json.NewDecoder(h.out).Decode(&jout)
|
|
span.Finish()
|
|
if err != nil {
|
|
return models.NewAPIError(http.StatusBadGateway, fmt.Errorf("invalid json response from function err: %v", err))
|
|
}
|
|
|
|
span, _ = opentracing.StartSpanFromContext(ctx, "dispatch_json_write_response")
|
|
defer span.Finish()
|
|
|
|
rw, ok := w.(http.ResponseWriter)
|
|
if !ok {
|
|
// logs can just copy the full thing in there, headers and all.
|
|
return json.NewEncoder(w).Encode(jout)
|
|
}
|
|
|
|
// this has to be done for pulling out:
|
|
// - status code
|
|
// - body
|
|
// - headers
|
|
if jout.Protocol != nil {
|
|
p := jout.Protocol
|
|
for k, v := range p.Headers {
|
|
for _, vv := range v {
|
|
rw.Header().Add(k, vv) // on top of any specified on the route
|
|
}
|
|
}
|
|
}
|
|
// after other header setting, top level content_type takes precedence and is
|
|
// absolute (if set). it is expected that if users want to set multiple
|
|
// values they put it in the string, e.g. `"content-type:"application/json; charset=utf-8"`
|
|
// TODO this value should not exist since it's redundant in proto headers?
|
|
if jout.ContentType != "" {
|
|
rw.Header().Set("Content-Type", jout.ContentType)
|
|
}
|
|
|
|
// we must set all headers before writing the status, see http.ResponseWriter contract
|
|
if p := jout.Protocol; p != nil && p.StatusCode != 0 {
|
|
rw.WriteHeader(p.StatusCode)
|
|
}
|
|
|
|
_, err = io.WriteString(rw, jout.Body)
|
|
return err
|
|
}
|