-2

I want to access a http.Request's Body multiple times. The first time happens in my authentication middleware, it uses it to recreate a sha256 signature. The second time happens later, I parse it into JSON for use in my database.

I realize that you can't read from an io.Reader (or an io.ReadCloser in this case) more than once. I found an answer to another question with a solution:

When you first read the body, you have to store it so once you're done with it, you can set a new io.ReadCloser as the request body constructed from the original data. So when you advance in the chain, the next handler can read the same body.

Then in the example they set http.Request.Body to a new io.ReadCloser:

// And now set a new body, which will simulate the same data we read:
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))

Reading from Body and then setting a new io.ReadCloser at each step in my middleware seems expensive. Is this accurate?

In an effort to make this less tedious and expensive I use a solution described here to stash the parsed byte array in the Context() value of the request. Whenever I want it, its waiting for me already as byte array:

type bodyKey int
const bodyAsBytesKey bodyKey = 0

func newContextWithParsedBody(ctx context.Context, req *http.Request) context.Context {
    if req.Body == nil || req.ContentLength <= 0 {
        return ctx
    }

    if _, ok := ctx.Value(bodyAsBytesKey).([]byte); ok {
        return ctx
    }

    body, err := ioutil.ReadAll(req.Body)
    if err != nil {
        return ctx
    }

    return context.WithValue(ctx, bodyAsBytesKey, body)
}

func parsedBodyFromContext(ctx context.Context) []byte {
    if body, ok := ctx.Value(bodyAsBytesKey).([]byte); ok {
        return body
    }

    return nil
}

I feel like keeping a single byte array around is cheaper than reading a new one each time. Is this accurate? Are there pitfalls to this solution that I can't see?

Shawn Throop
  • 1,281
  • 1
  • 13
  • 28
  • If you're putting the body into a `bytes.Reader` or `bytes.Buffer`, you are essentially just keeping the same cached byte slice. Are you trying to avoid copying it with `ReadAll`? – JimB Nov 29 '17 at 17:16
  • @JimB `ReadAll` was just the simplest way I've found to read everything from `Body` – Shawn Throop Nov 29 '17 at 17:19
  • 1
    I'm asking why you think putting the byte slice back into the body is expensive? Are you trying to avoid the copy, or something else? Simply wrapping the byte slice in a `Reader` and `Closer` is no more expensive than your context method. If you want to avoid the copy, make a simple type to give you direct access to the bytes from the body. – JimB Nov 29 '17 at 17:27
  • @JimB I assumed that reading from a `io.Reader` would involve some sort of conversion or at least be more expensive than accessing a value from a `Context`. Is that incorrect? – Shawn Throop Nov 29 '17 at 17:52
  • 1
    Yes, reading from the body will copy the bytes into the destination slice (there's no conversion). If you need a copy though, then you're going to do that anyways. If you don't need a copy, I would still stash it in the Body to remain compatible with any other code that isn't aware of the body in the context, and extract it from the Body interface rather than the context. – JimB Nov 29 '17 at 17:57
  • Read once. Put into a variable and use every time yoga need – Eugene Lisitsky Nov 29 '17 at 18:07

1 Answers1

1

Is it "cheaper"? Probably, depending on what resource(s) you're looking at, but you should benchmark and compare your specific application to know for sure. Are there pitfalls? Everything has pitfalls, this doesn't seem particularly "risky" to me, though. Context values are kind of a lousy solution to any problem due to loss of compile-time type checking and the general increase in complexity and loss of readability. You'll have to decide what trade-offs to make in your particular situation.

If you don't need the hash to be completed before the handler starts, you could also wrap the body reader in another reader (e.g. io.TeeReader), so that when you unmarshall the JSON, the wrapper can watch the bytes that are read and compute the signature hash. Is that "cheaper"? You'd have to benchmark & compare to know. Is it better? Depends entirely on your situation. It is an option worth considering.

Adrian
  • 42,911
  • 6
  • 107
  • 99