-
Notifications
You must be signed in to change notification settings - Fork 832
Harden HTTP Response Body Handling #4489
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ import ( | |
"compress/gzip" | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"time" | ||
|
@@ -74,6 +75,12 @@ func createHttpSender(httpClient *http.Client, endpoint config.AgmaAnalyticsHttp | |
glog.Errorf("[agmaAnalytics] Sending request failed %v", err) | ||
return err | ||
} | ||
defer func() { | ||
if _, err := io.Copy(io.Discard, resp.Body); err != nil { | ||
glog.Errorf("[agmaAnalytics] Draining response body failed: %v", err) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The response body must be fully read and closed to ensure connection reuse. Discarding the body is especially needed when the body content is ignored / not used. |
||
resp.Body.Close() | ||
}() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
glog.Errorf("[agmaAnalytics] Wrong code received %d instead of %d", resp.StatusCode, http.StatusOK) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,24 +2,38 @@ package pubstack | |
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"time" | ||
|
||
"github.com/docker/go-units" | ||
"github.com/golang/glog" | ||
) | ||
|
||
func fetchConfig(client *http.Client, endpoint *url.URL) (*Configuration, error) { | ||
res, err := client.Get(endpoint.String()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to add a timeout to this fetch while we are here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree we should. I don't know the pubstack analytics module well enough to implement it. Will reach out to pubstack to implement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discussed offline. I will reach out to pubstack to see what a reasonable timeout would be and we can update it in another PR. |
||
if err != nil { | ||
return nil, err | ||
} | ||
defer func() { | ||
// read the entire response body to ensure full connection reuse if there's an | ||
// error while decoding the json | ||
if _, err := io.Copy(io.Discard, res.Body); err != nil { | ||
glog.Errorf("[pubstack] Draining config response body failed: %v", err) | ||
} | ||
res.Body.Close() | ||
}() | ||
|
||
if res.StatusCode != http.StatusOK { | ||
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode) | ||
} | ||
|
||
defer res.Body.Close() | ||
c := Configuration{} | ||
err = json.NewDecoder(res.Body).Decode(&c) | ||
if err != nil { | ||
return nil, err | ||
return nil, fmt.Errorf("failed to decode config: %w", err) | ||
} | ||
return &c, nil | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ package eventchannel | |
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"path" | ||
|
@@ -27,7 +28,12 @@ func NewHttpSender(client *http.Client, endpoint string) Sender { | |
if err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to add a timeout just above to the http request? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree we should. I don't know the pubstack analytics module well enough to implement it. Will reach out to pubstack to implement. |
||
return err | ||
} | ||
resp.Body.Close() | ||
defer func() { | ||
if _, err := io.Copy(io.Discard, resp.Body); err != nil { | ||
glog.Errorf("[pubstack] Draining sender response body failed: %v", err) | ||
} | ||
resp.Body.Close() | ||
}() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
glog.Errorf("[pubstack] Wrong code received %d instead of %d", resp.StatusCode, http.StatusOK) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package currency | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
@@ -16,6 +17,7 @@ import ( | |
// RateConverter holds the currencies conversion rates dictionary | ||
type RateConverter struct { | ||
httpClient httpClient | ||
httpTimeout time.Duration | ||
staleRatesThreshold time.Duration | ||
syncSourceURL string | ||
rates atomic.Value // Should only hold Rates struct | ||
|
@@ -27,11 +29,13 @@ type RateConverter struct { | |
// NewRateConverter returns a new RateConverter | ||
func NewRateConverter( | ||
httpClient httpClient, | ||
httpTimeout time.Duration, | ||
syncSourceURL string, | ||
staleRatesThreshold time.Duration, | ||
) *RateConverter { | ||
return &RateConverter{ | ||
httpClient: httpClient, | ||
httpTimeout: httpTimeout, | ||
staleRatesThreshold: staleRatesThreshold, | ||
syncSourceURL: syncSourceURL, | ||
rates: atomic.Value{}, | ||
|
@@ -43,7 +47,10 @@ func NewRateConverter( | |
|
||
// fetch allows to retrieve the currencies rates from the syncSourceURL provided | ||
func (rc *RateConverter) fetch() (*Rates, error) { | ||
request, err := http.NewRequest("GET", rc.syncSourceURL, nil) | ||
ctx, cancel := context.WithTimeout(context.Background(), rc.httpTimeout) | ||
defer cancel() | ||
|
||
request, err := http.NewRequestWithContext(ctx, "GET", rc.syncSourceURL, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
@@ -52,23 +59,29 @@ func (rc *RateConverter) fetch() (*Rates, error) { | |
if err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to add a timeout to the http request just above? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Good catch. I added a configurable timeout with a default of 60 seconds. In test from my machine, latency is around 30ms. Since this isn't part of the real time auction flow, happy to set this to a higher limit considering there is no limit today. Hosts can reduce if they find it necessary or we can revisit the default later if there are no concerns right now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discussed offline. We can address this in a follow-up PR. |
||
return nil, err | ||
} | ||
defer func() { | ||
// read the entire response body to ensure full connection reuse if there's an | ||
// invalid status code | ||
if _, err := io.Copy(io.Discard, response.Body); err != nil { | ||
glog.Errorf("error draining conversion rates response body: %v", err) | ||
} | ||
response.Body.Close() | ||
}() | ||
|
||
if response.StatusCode >= 400 { | ||
message := fmt.Sprintf("The currency rates request failed with status code %d", response.StatusCode) | ||
message := fmt.Sprintf("the currency rates request failed with status code %d", response.StatusCode) | ||
return nil, &errortypes.BadServerResponse{Message: message} | ||
} | ||
|
||
defer response.Body.Close() | ||
|
||
bytesJSON, err := io.ReadAll(response.Body) | ||
if err != nil { | ||
return nil, err | ||
return nil, fmt.Errorf("the currency rates request failed: %v", err) | ||
} | ||
|
||
updatedRates := &Rates{} | ||
err = jsonutil.UnmarshalValid(bytesJSON, updatedRates) | ||
if err != nil { | ||
return nil, err | ||
return nil, fmt.Errorf("the currency rates request failed to parse json: %v", err) | ||
} | ||
|
||
return updatedRates, err | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not directly related. Updated the function signature since only a single error is used.