mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
possible breakages: * `FN_HEADER` on cold are no longer `s/-/_/` -- this is so that cold functions can rebuild the headers as they were when they came in on the request (fdks, specifically), there's no guarantee that a reversal `s/_/-/` is the original header on the request. * app and route config no longer `s/-/_/` -- it seemed really weird to rewrite the users config vars on these. should just pass them exactly as is to env. * headers no longer contain the environment vars (previously, base config; app config, route config, `FN_PATH`, etc.), these are still available in the environment. this gets rid of a lot of the code around headers, specifically the stuff that shoved everything into headers when constructing a call to begin with. now we just store the headers separately and add a few things, like FN_CALL_ID to them, and build a separate 'config' now to store on the call. I thought 'config' was more aptly named, 'env' was confusing, though now 'config' is exactly what 'base_vars' was, which is only the things being put into the env. we weren't storing this field in the db, this doesn't break unless there are messages in a queue from another version, anyway, don't think we're there and don't expect any breakage for anybody with field name changes. this makes the configuration stuff pretty straight forward, there's just two separate buckets of things, and cold just needs to mash them together into the env, and otherwise hot containers just need to put 'config' in the env, and then hot format can shove 'headers' in however they'd like. this seems better than my last idea about making this easier but worse (RIP). this means: * headers no longer contain all vars, the set of base vars can only be found in the environment. * headers is only the headers from request + call_id, deadline, method, url * for cold, we simply add the headers to the environment, prepending `FN_HEADER_` to them, BUT NOT upper casing or `s/-/_/` * fixes issue where async hot functions would end up with `Fn_header_` prefixed headers * removes idea of 'base' vars and 'env'. this was a strange concept. now we just have 'config' which was base vars, and headers, which was base_env+headers; i.e. they are disjoint now. * casing for all headers will lean to be `My-Header` style, which should help with consistency. notable exceptions for cold only are FN_CALL_ID, FN_METHOD, and FN_REQUEST_URL -- this is simply to avoid breakage, in either hot format they appear as `Fn_call_id` still. * removes FN_PARAM stuff * updated doc with behavior weird things left: `Fn_call_id` e.g. isn't a correctly formatted http header, it should likely be `Fn-Call-Id` but I wanted to live to fight another day on this one, it would add some breakage. examples to be posted of each format below closes #329
192 lines
4.5 KiB
Go
192 lines
4.5 KiB
Go
package protocol
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
)
|
|
|
|
// This is sent into the function
|
|
// All HTTP request headers should be set in env
|
|
type jsonio struct {
|
|
Body string `json:"body"`
|
|
ContentType string `json:"content_type"`
|
|
}
|
|
|
|
// CallRequestHTTP for the protocol that was used by the end user to call this function. We only have HTTP right now.
|
|
type CallRequestHTTP struct {
|
|
// TODO request method ?
|
|
Type string `json:"type"`
|
|
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 {
|
|
jsonio
|
|
CallID string `json:"call_id"`
|
|
Protocol *CallRequestHTTP `json:"protocol"`
|
|
}
|
|
|
|
// jsonOut the expected response from the function container
|
|
type jsonOut struct {
|
|
jsonio
|
|
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 writeString(err error, dst io.Writer, str string) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.WriteString(dst, str)
|
|
return err
|
|
}
|
|
|
|
// TODO(xxx): headers, query parameters, body - what else should we add to func's payload?
|
|
// TODO(xxx): get rid of request body buffering somehow
|
|
// @treeder: I don't know why we don't just JSON marshal this, this is rough...
|
|
func (h *JSONProtocol) writeJSONToContainer(ci CallInfo) error {
|
|
stdin := json.NewEncoder(h.in)
|
|
bb := new(bytes.Buffer)
|
|
_, err := bb.ReadFrom(ci.Input())
|
|
// todo: better/simpler err handling
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// open
|
|
err = writeString(err, h.in, "{\n")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// call_id
|
|
err = writeString(err, h.in, `"call_id":`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = stdin.Encode(ci.CallID())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// content_type
|
|
err = writeString(err, h.in, ",")
|
|
err = writeString(err, h.in, `"content_type":`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = stdin.Encode(ci.ContentType())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// body
|
|
err = writeString(err, h.in, ",")
|
|
err = writeString(err, h.in, `"body":`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = stdin.Encode(bb.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// now the extras
|
|
err = writeString(err, h.in, ",")
|
|
err = writeString(err, h.in, `"protocol":{`) // OK name? This is what OpenEvents is calling it in initial proposal
|
|
{
|
|
err = writeString(err, h.in, `"type":`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = stdin.Encode(ci.ProtocolType())
|
|
|
|
// request URL
|
|
err = writeString(err, h.in, ",")
|
|
err = writeString(err, h.in, `"request_url":`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = stdin.Encode(ci.RequestURL())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// HTTP headers
|
|
err = writeString(err, h.in, ",")
|
|
err = writeString(err, h.in, `"headers":`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = stdin.Encode(ci.Headers())
|
|
}
|
|
err = writeString(err, h.in, "}")
|
|
|
|
// close
|
|
err = writeString(err, h.in, "\n}\n\n")
|
|
return err
|
|
}
|
|
|
|
func (h *JSONProtocol) Dispatch(ctx context.Context, ci CallInfo, w io.Writer) error {
|
|
// write input into container
|
|
err := h.writeJSONToContainer(ci)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// now read the container output
|
|
jout := new(jsonOut)
|
|
dec := json.NewDecoder(h.out)
|
|
if err := dec.Decode(jout); err != nil {
|
|
return fmt.Errorf("error decoding JSON from user function: %v", err)
|
|
}
|
|
if rw, ok := w.(http.ResponseWriter); ok {
|
|
// 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
|
|
}
|
|
}
|
|
if p.StatusCode != 0 {
|
|
rw.WriteHeader(p.StatusCode)
|
|
}
|
|
}
|
|
_, err = io.WriteString(rw, jout.Body) // TODO timeout
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// logs can just copy the full thing in there, headers and all.
|
|
err = json.NewEncoder(w).Encode(jout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|