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

[fix] httpbp middleware doesn't flush chunked responses #573

Merged

Conversation

adamthesax
Copy link
Contributor

@adamthesax adamthesax commented Oct 24, 2022

Closes #

💸 TL;DR

httpbp middleware, in particular RecordStatusCode and PrometheusServerMetrics don't work with chunked HTTP responses because they wrap http.ResponseWriter and do not implement http.Flush. This updates our response wrapping within in the Middleware to support both the http.Flush and http.Hijacker interface.

📜 Details

Design Doc

Jira

🧪 Testing Steps / Validation

✅ Checks

  • CI tests (if present) are passing
  • Adheres to code style for repo
  • Contributor License Agreement (CLA) completed if not a Reddit employee

@adamthesax adamthesax force-pushed the fix-add-flush-to-httpbp-middleware branch from c13ff31 to 97c1121 Compare October 24, 2022 18:56
@adamthesax adamthesax changed the title [fix] httpbp middleware breaks chunked reponses [fix] httpbp middleware doesn't flush chunked responses Oct 24, 2022
Copy link
Contributor

@kylelemons kylelemons left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

I think the only thing I would request is to make a unit test that makes a dummy server with the default middleware chain and makes sure that the handler can be hijacked and (separately) flushed.

httpbp/middlewares.go Outdated Show resolved Hide resolved
@adamthesax adamthesax marked this pull request as ready for review October 24, 2022 21:17
@adamthesax adamthesax requested a review from a team as a code owner October 24, 2022 21:17
@adamthesax adamthesax requested review from pacejackson and removed request for a team October 24, 2022 21:17
@adamthesax adamthesax requested review from kylelemons and removed request for fishy and pacejackson October 24, 2022 21:24
httpbp/middlewares_test.go Show resolved Hide resolved
Comment on lines 517 to 547
type wrappedHijacker struct {
http.ResponseWriter
http.Hijacker
}

type wrappedFlusher struct {
http.ResponseWriter
http.Flusher
}

type wrappedFlushHijacker struct {
http.ResponseWriter
http.Flusher
http.Hijacker
}

func allowFlushHijack(original, rw http.ResponseWriter) http.ResponseWriter {
flusher, isFlusher := original.(http.Flusher)
hijacker, isHijacker := original.(http.Hijacker)
switch {
case isFlusher && isHijacker:
return &wrappedFlushHijacker{rw, flusher, hijacker}
case isFlusher:
return allowFlush(original, rw)
case isHijacker:
return allowHijack(original, rw)
default:
return rw
}

}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔕 it's manage-able as we currently have 2 optional interfaces, it will become a nightmare if there's a 3rd one added in the future. but I also can't find a better way to do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this approach is definitely verbose but I can't think of a better one. The only other interface on http that a http.ResponseWriter could implement would be http.Pusher. I don't think it's worth supporting it now as I don't think we have any use case driving it, but that would be a 3rd interface that may come up in the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a quietly known issue amongst the ecosystem, and the approach in the generic case seems to be code generation.

https://www.doxsey.net/blog/fixing-interface-erasure-in-go/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the article! I updated this PR to use the approach outlined in the article and included http.Pusher. Because we are dealing with 3 interfaces, I adapted their code generator and then massaged the output to make it more human readable. Basically leveraging the jump table idea, without having to adapt a full code generator.

@kylelemons let me know if you think is approach is better or if we should revert back to my previous one.

Copy link
Contributor

@kylelemons kylelemons left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally prefer the previous approach. It's true that Pusher is missed, but it has the benefit that the type names wouldn't be atrocious :)

That said, I have no issue with keeping this. The approach is sound, and it's more complete.

@@ -0,0 +1,33 @@
// DO NOT EDIT.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DO NOT EDIT is incompatible with "this was generated and then edited" :D

Copy link
Member

@fishy fishy Oct 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1.

I would say it's better to just hand write this code:

type optionalResponseWriter int

const (
  flusher optionalResponseWriter = 1 << iota  
  hijacker
  pusher
)

func wrapResponseWriter(...) ... {
  var f optionalResponseWriter
  ...

  switch f {
  case 0:
    ...
  case flusher:
    ...
  case flusher | hijacker:
    ...
  ...
}

@kylelemons kylelemons requested a review from fishy October 27, 2022 21:43
Copy link
Member

@fishy fishy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong preference between this and the previous approach, but if we pick this approach, I'd prefer to hand writing the code over using codegen.

@@ -0,0 +1,33 @@
// DO NOT EDIT.
Copy link
Member

@fishy fishy Oct 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1.

I would say it's better to just hand write this code:

type optionalResponseWriter int

const (
  flusher optionalResponseWriter = 1 << iota  
  hijacker
  pusher
)

func wrapResponseWriter(...) ... {
  var f optionalResponseWriter
  ...

  switch f {
  case 0:
    ...
  case flusher:
    ...
  case flusher | hijacker:
    ...
  ...
}

@adamthesax adamthesax requested a review from fishy October 31, 2022 15:58
httpbp/response_wrappers.go Outdated Show resolved Hide resolved
@kylelemons kylelemons merged commit fae00cb into reddit:master Nov 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants