Skip to content

Feat/http client - #47

Open
dev-bramwel wants to merge 5 commits into
Flying-Tea-Squad:mainfrom
dev-bramwel:feat/http-client
Open

dev-bramwel wants to merge 5 commits into
Flying-Tea-Squad:mainfrom
dev-bramwel:feat/http-client

Conversation

@dev-bramwel

Copy link
Copy Markdown
Contributor

This addition resolves #6

@carsonak

Copy link
Copy Markdown
Collaborator

Hello, kindly fix the failing linter tests or convert this PR to a draft if you are still working on it.

@dev-bramwel
dev-bramwel marked this pull request as draft July 29, 2026 13:24
@dev-bramwel
dev-bramwel marked this pull request as ready for review July 30, 2026 12:11
@dev-bramwel

Copy link
Copy Markdown
Contributor Author

This PR resolves all errcheck static analysis warnings raised by golangci-lint in http.go and http_test.go by explicitly suppressing expected/non-critical error return values using _, _ = or _ =.

http.go:

  • Explicitly ignored error on io.Copy(io.Discard, resp.Body) (draining response body for keep-alive connection reuse).

  • Explicitly ignored errors on c.dumpWriter.Write(...) (debug/trace logging side-effects).

  • Explicitly ignored error on resp.Body.Close().

http_test.go:

  • Explicitly ignored errors on w.Write(...) calls in test HTTP handler mocks writing to in-memory buffers.

  • Wrapped deferred resp.Body.Close() calls in anonymous functions with explicit error suppression (defer func() { _ = resp.Body.Close() }()).

These unhandled return values were non-critical side effects or safe test operations where handling errors programmatically is unnecessary. Explicitly marking them as ignored ensures clear intent for maintainers and satisfies the errcheck linter rule, allowing CI workflow checks to pass cleanly.

Testing

Ran golangci-lint run locally—all errcheck warnings in the repository are now cleared.

Comment thread http.go
}

var lastErr error
for attempt := 1; attempt <= attempts; attempt++ {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Avoid automatically retrying for non-idempotent http methods such as POST and PATCH, unlessan idempotency key has been provided.

Comment thread http.go
}

if err := sleepBeforeRetry(ctx, c.retryBaseDelay, attempt); err != nil {
if lastErr != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Instead of choosing to return one error over the other, return both of them by via error wrapping. For example:

return nil, fmt.Errorf("%w: previous request error: %v", err, lastErr)

Comment thread http.go
"error", err,
)

if !shouldRetryError(err) || attempt == attempts {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Avoid automatic retry after a 429 Too Many Requests. For this case retry after the time given in the Retry-After header or exit with an error.

Comment thread http.go
}

func shouldRetryError(err error) bool {
return err != nil

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not all errors should cause a retry. Some errors will not change the response from the server even after retrying.

Comment thread http.go
return nil, lastErr
}

func makeRequestBodyReplayable(req *http.Request) error {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

makeRequestBodyReplayable reads the entire request body into memory using io.ReadAll. This creates an unbounded allocation and silently changes streaming requests into fully buffered requests. For sufficiently large bodies (e.g, from large files), this could cause excessive memory use or an out-of-memory failure. If body size can be influenced by untrusted input, it may also become a denial-of-service risk.

I suggest removing makeRequestBodyReplayable rather than automatically buffering arbitrary bodies.

For requests with a body, automatic retries should require req.GetBody method to be non-nil so that a fresh body can be created for each attempt without loading the complete payload into memory. When req.Body != nil && req.GetBody == nil, the client:

  1. sends the request once.
  2. if the request fails, return a clear error explaining that the body cannot be replayed for retries.

For example:

if attempts > 1 && req.Body != nil && req.GetBody == nil {
    return nil, errors.New(
        "paykit: request body cannot be replayed; provide GetBody or disable retries",
    )
}

@carsonak carsonak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great work! I have requested some changes, kindly take a look at them and fix the issues and add tests for the fixes. Once done you can push the commits to this PR and mark the changes as resolved so I can merge this PR.

@dev-bramwel

Copy link
Copy Markdown
Contributor Author

I will review them

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement HTTP client with retry, TLS, and logging

3 participants