Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: CI

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"

- name: Run tests
run: go test ./...
36 changes: 24 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,47 @@ go get github.com/fervbmx/interceptor

## Usage

Import both packages:

```go
import (
"net/http"

"github.com/fervbmx/interceptor"
"github.com/fervbmx/interceptor/interceptors"
)
```

```go
// Flow: HeaderInterceptorBasicAuthInterceptor → http.DefaultTransport
// Flow: RequestLoggingHeader → BasicAuth → http.DefaultTransport
client := &http.Client{
Transport: interceptor.NewTransportInterceptor(
Transport: interceptor.NewTransport(
nil,
interceptors.HeaderInterceptor("X-API-KEY", "secret"),
interceptors.BasicAuthInterceptor("user", "pass"),
interceptors.Header("X-API-KEY", "secret"),
interceptors.BasicAuth("user", "pass"),
interceptors.RequestLogging(nil),
),
}
```

Pass `nil` as the first argument to use `http.DefaultTransport`, or provide your own `http.RoundTripper`.
Pass `nil` as the first argument to use `http.DefaultTransport`, or pass a custom `http.RoundTripper` as the base transport.

## Built-in interceptors

| Interceptor | Description |
|---|---|
| `HeaderInterceptor(key, value)` | Sets a header on every request |
| `BasicAuthInterceptor(user, password)` | Sets Basic authentication |
| `Header(key, value)` | Sets a header on every request |
| `BasicAuth(user, password)` | Sets Basic authentication |
| `RequestLogging(opts)` | Emits structured `slog` attributes |

## Custom interceptors

Write your own `InterceptorFunc` to hook into the request/response lifecycle. Call `next` to continue the chain, or return early to short-circuit it.
Write your own `interceptor.Middleware` to hook into the request/response lifecycle. Call `next` to continue the chain, or return early to short-circuit it.

```go
// Log every request and its status code.
func LogginInterceptor(req *http.Request, next interceptor.HandlerFunc) (*http.Response, error) {
func Logging(req *http.Request, next interceptor.HandlerFunc) (*http.Response, error) {
log.Printf("→ %s %s", req.Method, req.URL)

resp, err := next(req)
if err != nil {
return nil, err
Expand All @@ -48,9 +60,9 @@ func LogginInterceptor(req *http.Request, next interceptor.HandlerFunc) (*http.R
}

client := &http.Client{
Transport: interceptor.NewTransportInterceptor(
Transport: interceptor.NewTransport(
http.DefaultTransport,
interceptor.LogginInterceptor
Logging,
),
}
```
Expand Down
14 changes: 7 additions & 7 deletions interceptors/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import (
"github.com/fervbmx/interceptor"
)

// BasicAuthInterceptor returns an interceptor that sets Basic authentication on every
// BasicAuth returns an interceptor that sets Basic authentication on every
// outgoing request.
//
// interceptor.NewTransportInterceptor(nil,
// interceptors.BasicAuthInterceptor("username", "password"),
// interceptor.NewTransport(nil,
// interceptors.BasicAuth("username", "password"),
// )
func BasicAuthInterceptor(username, password string) interceptor.InterceptorFunc {
func BasicAuth(username, password string) interceptor.Middleware {
return func(req *http.Request, next interceptor.HandlerFunc) (*http.Response, error) {
req = req.Clone(req.Context())
req.SetBasicAuth(username, password)
return next(req)
clonedReq := req.Clone(req.Context())
clonedReq.SetBasicAuth(username, password)
return next(clonedReq)
}
}
21 changes: 11 additions & 10 deletions interceptors/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/fervbmx/interceptor/interceptors"
)

func TestBasicAuthInterceptor(t *testing.T) {
func TestBasicAuth(t *testing.T) {
cases := []struct {
name string
username string
Expand All @@ -28,8 +28,8 @@ func TestBasicAuthInterceptor(t *testing.T) {
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var username, password string
var ok bool

Expand All @@ -39,9 +39,9 @@ func TestBasicAuthInterceptor(t *testing.T) {
t.Cleanup(server.Close)

client := http.Client{
Transport: interceptor.NewTransportInterceptor(
Transport: interceptor.NewTransport(
http.DefaultTransport,
interceptors.BasicAuthInterceptor(c.username, c.password),
interceptors.BasicAuth(tc.username, tc.password),
),
Timeout: 15 * time.Second,
}
Expand All @@ -50,21 +50,22 @@ func TestBasicAuthInterceptor(t *testing.T) {
if err != nil {
t.Fatalf("client.Get() returned error: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected status code: %d", resp.StatusCode)
}

if !ok {
t.Fatal("BasicAuth() returned ok=false, want true")
}

if username != c.username {
t.Errorf("username = %q, want %q", username, c.username)
if username != tc.username {
t.Errorf("username = %q, want %q", username, tc.username)
}

if password != c.password {
t.Errorf("password = %q, want %q", password, c.password)
if password != tc.password {
t.Errorf("password = %q, want %q", password, tc.password)
}
})
}
Expand Down
14 changes: 7 additions & 7 deletions interceptors/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import (
"github.com/fervbmx/interceptor"
)

// HeaderInterceptor returns an interceptor that sets a header on every outgoing
// Header returns an interceptor that sets a header on every outgoing
// request.
//
// interceptor.NewTransportInterceptor(nil,
// interceptors.HeaderInterceptor("User-Agent", "MyApp/1.0"),
// interceptor.NewTransport(nil,
// interceptors.Header("User-Agent", "MyApp/1.0"),
// )
func HeaderInterceptor(key, value string) interceptor.InterceptorFunc {
func Header(key, value string) interceptor.Middleware {
return func(req *http.Request, next interceptor.HandlerFunc) (*http.Response, error) {
req = req.Clone(req.Context())
req.Header.Set(key, value)
return next(req)
clonedReq := req.Clone(req.Context())
clonedReq.Header.Set(key, value)
return next(clonedReq)
}
}
17 changes: 9 additions & 8 deletions interceptors/headers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ func TestHeaderInterceptor(t *testing.T) {
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var header string

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header = r.Header.Get(c.key)
header = r.Header.Get(tc.key)
}))
t.Cleanup(server.Close)

client := http.Client{
Transport: interceptor.NewTransportInterceptor(
Transport: interceptor.NewTransport(
http.DefaultTransport,
interceptors.HeaderInterceptor(c.key, c.value),
interceptors.Header(tc.key, tc.value),
),
Timeout: 15 * time.Second,
}
Expand All @@ -54,13 +54,14 @@ func TestHeaderInterceptor(t *testing.T) {
if err != nil {
t.Fatalf("client.Get() returned error: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected status code: %d", resp.StatusCode)
}

if header != c.value {
t.Errorf("Header %q = %q, want %q", c.key, header, c.value)
if header != tc.value {
t.Errorf("Header %q = %q, want %q", tc.key, header, tc.value)
}
})
}
Expand Down
Loading
Loading