Adding to a values to a context via an endpoint and retrieving them on the encoders

72 views
Skip to first unread message

Atahualpa Ledesma

unread,
Feb 21, 2019, 8:23:18 PM2/21/19
to Go kit

I have a custom endpoint that needs to add some additional http headers to every response, I was able to add the header values to the context, but I am unable to get those values from either the encode error function or the encode response function, but the values are correctly displayed in the actual service.

What is the correct way to add values to the context and have them available for consumption on the error and response encoders? I also tried to read these values from a ServerAfter func to no luck.


Example:

on my custom endpoint:


type contextKey int

const (
    ContextKeyFoo = iota
    ContextKeyBar
)

func NewCustomFoo() endpoint.Middleware {
    return func(next endpoint.Endpoint) endpoint.Endpoint {
        return func(ctx context.Context, request interface{}) (interface{}, error) {

for k, v := range map[contextKey]string{
               ContextKeyFoo:     "foo",
               ContextKeyBar: "bar",
           } {
               ctx = context.WithValue(ctx, k, v)
           }

/* some other stuff */


return next(ctx, request)

}
    }
}


Now if I try to print it on the service everything looks ok, not so on the response or error encoders,

spew.Dump(ctx.Value(ContextKeyFoo))



Atahualpa Ledesma

unread,
Feb 22, 2019, 11:57:09 AM2/22/19
to Go kit
Here is some sample code that shows with more detail what I am trying to achieve, and maybe using the context is a bad way to do it:


package main

import (
   "context"
   "encoding/json"
   "errors"
   "log"
   "math/rand"
   "net/http"
   "strconv"
   "strings"
   "time"

)

type uppercaseRequest struct {
   S string `json:"s"`
}

type uppercaseResponse struct {
   V   string `json:"v"`
   Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}

type countRequest struct {
   S string `json:"s"`
}

type countResponse struct {
   V int `json:"v"`
}

// StringService provides operations on strings.
type StringService interface {
   Uppercase(context.Context, string) (string, error)
   Count(context.Context, string) int
}

type stringService struct{}

type contextKey int

// Service context keys
const (
   ContextKeyMyRandomID contextKey = iota
)

func (stringService) Uppercase(ctx context.Context, s string) (string, error) {

    id, ok := ctx.Value(ContextKeyMyRandomID).(string)
   if ok {
       log.Printf("Uppercase Request with Custom ID: %s", id)
   }

    if s == "" {
       return "", ErrEmpty
   }
   return strings.ToUpper(s), nil
}

func (stringService) Count(ctx context.Context, s string) int {
   id, ok := ctx.Value(ContextKeyMyRandomID).(string)
   if ok {
       log.Printf("Count Request with Custom ID: %s", id)
   }
   return len(s)
}

// ErrEmpty is returned when input string is empty
var ErrEmpty = errors.New("Empty string")

func makeUppercaseEndpoint(svc StringService) endpoint.Endpoint {
   return func(ctx context.Context, request interface{}) (interface{}, error) {
       req := request.(uppercaseRequest)
       v, err := svc.Uppercase(ctx, req.S)
       if err != nil {
           return uppercaseResponse{v, err.Error()}, nil
       }
       return uppercaseResponse{v, ""}, nil
   }
}

func makeCountEndpoint(svc StringService) endpoint.Endpoint {
   return func(ctx context.Context, request interface{}) (interface{}, error) {
       req := request.(countRequest)
       v := svc.Count(ctx, req.S)
       return countResponse{v}, nil
   }
}

func main() {

    var (
       uppercaseEndpoint endpoint.Endpoint
       countEndpoint     endpoint.Endpoint
       svc               stringService
   )

    rand.Seed(time.Now().Unix())

    svc = stringService{}

    uppercaseEndpoint = makeUppercaseEndpoint(svc)
   uppercaseEndpoint = annotate("fourth")(uppercaseEndpoint)
   uppercaseEndpoint = myCustomMiddleware("third")(uppercaseEndpoint)
   uppercaseEndpoint = annotate("second")(uppercaseEndpoint)
   uppercaseEndpoint = annotate("first")(uppercaseEndpoint)

    countEndpoint = makeCountEndpoint(svc)
   countEndpoint = annotate("fifth")(countEndpoint)
   countEndpoint = annotate("fourth")(countEndpoint)
   countEndpoint = myCustomMiddleware("third")(countEndpoint)
   countEndpoint = annotate("second")(countEndpoint)
   countEndpoint = annotate("first")(countEndpoint)

    uppercaseHandler := httptransport.NewServer(
       uppercaseEndpoint,
       decodeUppercaseRequest,
       encodeResponse,
   )

    countHandler := httptransport.NewServer(
       countEndpoint,
       decodeCountRequest,
       encodeResponse,
   )

    http.Handle("/uppercase", uppercaseHandler)
   http.Handle("/count", countHandler)
   log.Println("Listening for requests...")
   log.Fatal(http.ListenAndServe(":8080", nil))
}

func decodeUppercaseRequest(ctx context.Context, r *http.Request) (interface{}, error) {
   var request uppercaseRequest
   if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
       return nil, err
   }
   return request, nil
}

func decodeCountRequest(ctx context.Context, r *http.Request) (interface{}, error) {
   var request countRequest
   if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
       return nil, err
   }
   return request, nil
}

func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {

    // Print the custom ID on a http.Header
   id, ok := ctx.Value(ContextKeyMyRandomID).(string)
   if ok {
       w.Header().Add("X-MyCustom-Id", id)
   }
   return json.NewEncoder(w).Encode(response)
}

func myCustomMiddleware(s string) endpoint.Middleware {
   return func(next endpoint.Endpoint) endpoint.Endpoint {
       return func(ctx context.Context, request interface{}) (interface{}, error) {
           log.Printf("endpoint: %s - %s", s, "pre")
           defer log.Printf("endpoint: %s - %s", s, "post")
           var (
               id int64
           )
           // Generate random number
           id = rand.Int63()
           // Add number to context
           for k, v := range map[contextKey]string{
               ContextKeyMyRandomID: strconv.FormatInt(id, 10),
           } {
               ctx = context.WithValue(ctx, k, v)
           }
           // Return the context
           return next(ctx, request)
       }
   }
}

func annotate(s string) endpoint.Middleware {
   return func(next endpoint.Endpoint) endpoint.Endpoint {
       return func(ctx context.Context, request interface{}) (interface{}, error) {
           log.Printf("endpoint: %s - %s", s, "pre")
           defer log.Printf("endpoint: %s - %s", s, "post")
           return next(ctx, request)
       }
   }
}



Request and Response via Curl:

curl -v -XPOST -d'{"s":"hello, world"}' localhost:8080/uppercase
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /uppercase HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Length: 20
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 20 out of 20 bytes
< HTTP/1.1 200 OK
< Date: Fri, 22 Feb 2019 16:52:08 GMT
< Content-Length: 21
< Content-Type: text/plain; charset=utf-8
<
{"v":"HELLO, WORLD"}
* Connection #0 to host localhost left intact


Program output:

2019/02/22 11:52:07 Listening for requests...
2019/02/22 11:52:08 endpoint: first - pre
2019/02/22 11:52:08 endpoint: second - pre
2019/02/22 11:52:08 endpoint: third - pre
2019/02/22 11:52:08 endpoint: fourth - pre
2019/02/22 11:52:08 Uppercase Request with Custom ID: 6315398990658663579
2019/02/22 11:52:08 endpoint: fourth - post
2019/02/22 11:52:08 endpoint: third - post
2019/02/22 11:52:08 endpoint: second - post
2019/02/22 11:52:08 endpoint: first - post

Does this now make more sense?

Alejandro Delgado Garcia

unread,
Mar 3, 2019, 10:32:21 PM3/3/19
to Go kit
Hi,

I think the context is meant to be used for requests that is probably why you can't read your custom value once you are processing your response. Probably at some point in the server request/response flow the context being given to you is a different one which does not has your desired value.

If you add a log in your encodeResponse function you will see  "ok" is false because the value is actually nil so is not possible to cast it to string.

If you check the source code of the context.go Value function you will see that they say

// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
Value(key interface{}) interface{}


Below you can see what I mean, I added some changes. Is not very pretty but it could be a quick solution.

package main

import (
   "context"
   "encoding/json"
   "errors"
   "fmt"
   "log"
   "math/rand"
   "net/http"
   "strconv"
   "strings"
   "time"

)

type uppercaseRequest struct {
   S string `json:"s"`
}

type uppercaseResponse struct {
   V   string `json:"v"`
   ID  string `json:"-"`             // You could use this hide the ID
   Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}

type countRequest struct {
   S string `json:"s"`
}

type countResponse struct {
   V int `json:"v"`
}

// StringService provides operations on strings.
type StringService interface {
   Uppercase(context.Context, string) (*uppercaseResponse, error)
   Count(context.Context, string) int
}

type stringService struct{}

type contextKey int

// Service context keys
const (
   ContextKeyMyRandomID contextKey = iota
)

func (stringService) Uppercase(ctx context.Context, s string) (*uppercaseResponse, error) {
   id, ok := ctx.Value(ContextKeyMyRandomID).(string)
   if ok {
       log.Printf("Uppercase Request with Custom ID: %s", id)
   }

    if s == "" {
       return nil, ErrEmpty
   }

    return &uppercaseResponse{
       V:   strings.ToUpper(s),
       ID:  id,
       Err: "",
   }, nil
}

func (stringService) Count(ctx context.Context, s string) int {
   id, ok := ctx.Value(ContextKeyMyRandomID).(string)
   if ok {
       log.Printf("Count Request with Custom ID: %s", id)
   }
   return len(s)
}

// ErrEmpty is returned when input string is empty
var ErrEmpty = errors.New("Empty string")

func makeUppercaseEndpoint(svc StringService) endpoint.Endpoint {
   return func(ctx context.Context, request interface{}) (interface{}, error) {
       req := request.(uppercaseRequest)
       v, err := svc.Uppercase(ctx, req.S)
       if err != nil {
           return uppercaseResponse{v.V, v.ID, err.Error()}, nil
       }
       return uppercaseResponse{v.V, v.ID, ""}, nil
   req, ok := response.(uppercaseResponse)
   if ok {
       fmt.Println("encodeResponse" + req.ID)
       w.Header().Add("X-MyCustom-Id", req.ID)
Message has been deleted

Alejandro

unread,
Mar 3, 2019, 10:39:29 PM3/3/19
to Go kit
If you don't see the full code let me know, you might now to show the original or show quoted text.

Regards
Reply all
Reply to author
Forward
0 new messages