Skip to content

Commit

Permalink
feat: add http.WithHeaderComment middleware (#461)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucix-aws authored Oct 6, 2023
1 parent 5180f26 commit 9067dec
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .changelog/a33cd9f8b5d5438a8f8140ee4f39c2f7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "a33cd9f8-b5d5-438a-8f81-40ee4f39c2f7",
"type": "feature",
"description": "Add `http.WithHeaderComment` middleware.",
"modules": [
"."
]
}
81 changes: 81 additions & 0 deletions transport/http/middleware_header_comment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package http

import (
"context"
"fmt"
"net/http"

"github.com/aws/smithy-go/middleware"
)

// WithHeaderComment instruments a middleware stack to append an HTTP field
// comment to the given header as specified in RFC 9110
// (https://www.rfc-editor.org/rfc/rfc9110#name-comments).
//
// The header is case-insensitive. If the provided header exists when the
// middleware runs, the content will be inserted as-is enclosed in parentheses.
//
// Note that per the HTTP specification, comments are only allowed in fields
// containing "comment" as part of their field value definition, but this API
// will NOT verify whether the provided header is one of them.
//
// WithHeaderComment MAY be applied more than once to a middleware stack and/or
// more than once per header.
func WithHeaderComment(header, content string) func(*middleware.Stack) error {
return func(s *middleware.Stack) error {
m, err := getOrAddHeaderComment(s)
if err != nil {
return fmt.Errorf("get or add header comment: %v", err)
}

m.values.Add(header, content)
return nil
}
}

type headerCommentMiddleware struct {
values http.Header // hijack case-insensitive access APIs
}

func (*headerCommentMiddleware) ID() string {
return "headerComment"
}

func (m *headerCommentMiddleware) HandleBuild(ctx context.Context, in middleware.BuildInput, next middleware.BuildHandler) (
out middleware.BuildOutput, metadata middleware.Metadata, err error,
) {
r, ok := in.Request.(*Request)
if !ok {
return out, metadata, fmt.Errorf("unknown transport type %T", in.Request)
}

for h, contents := range m.values {
for _, c := range contents {
if existing := r.Header.Get(h); existing != "" {
r.Header.Set(h, fmt.Sprintf("%s (%s)", existing, c))
}
}
}

return next.HandleBuild(ctx, in)
}

func getOrAddHeaderComment(s *middleware.Stack) (*headerCommentMiddleware, error) {
id := (*headerCommentMiddleware)(nil).ID()
m, ok := s.Build.Get(id)
if !ok {
m := &headerCommentMiddleware{values: http.Header{}}
if err := s.Build.Add(m, middleware.After); err != nil {
return nil, fmt.Errorf("add build: %v", err)
}

return m, nil
}

hc, ok := m.(*headerCommentMiddleware)
if !ok {
return nil, fmt.Errorf("existing middleware w/ id %s is not *headerCommentMiddleware", id)
}

return hc, nil
}
113 changes: 113 additions & 0 deletions transport/http/middleware_header_comment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package http

import (
"context"
"net/http"
"testing"

"github.com/aws/smithy-go/middleware"
)

func TestWithHeaderComment_CaseInsensitive(t *testing.T) {
stack, err := newTestStack(
WithHeaderComment("foo", "bar"),
)
if err != nil {
t.Errorf("expected no error on new stack, got %v", err)
}

r := injectBuildRequest(stack)
r.Header.Set("Foo", "baz")

if err := handle(stack); err != nil {
t.Errorf("expected no error on handle, got %v", err)
}

expectHeader(t, r.Header, "Foo", "baz (bar)")
}

func TestWithHeaderComment_Noop(t *testing.T) {
stack, err := newTestStack(
WithHeaderComment("foo", "bar"),
)
if err != nil {
t.Errorf("expected no error on new stack, got %v", err)
}

r := injectBuildRequest(stack)

if err := handle(stack); err != nil {
t.Errorf("expected no error on handle, got %v", err)
}

expectHeader(t, r.Header, "Foo", "")
}

func TestWithHeaderComment_MultiCaseInsensitive(t *testing.T) {
stack, err := newTestStack(
WithHeaderComment("foo", "c1"),
WithHeaderComment("Foo", "c2"),
WithHeaderComment("baz", "c3"),
WithHeaderComment("Baz", "c4"),
)
if err != nil {
t.Errorf("expected no error on new stack, got %v", err)
}

r := injectBuildRequest(stack)
r.Header.Set("Foo", "1")
r.Header.Set("Baz", "2")

if err := handle(stack); err != nil {
t.Errorf("expected no error on handle, got %v", err)
}

expectHeader(t, r.Header, "Foo", "1 (c1) (c2)")
expectHeader(t, r.Header, "Baz", "2 (c3) (c4)")
}

func newTestStack(fns ...func(*middleware.Stack) error) (*middleware.Stack, error) {
s := middleware.NewStack("", NewStackRequest)
for _, fn := range fns {
if err := fn(s); err != nil {
return nil, err
}
}
return s, nil
}

func handle(stack *middleware.Stack) error {
_, _, err := middleware.DecorateHandler(
middleware.HandlerFunc(
func(ctx context.Context, input interface{}) (
interface{}, middleware.Metadata, error,
) {
return nil, middleware.Metadata{}, nil
},
),
stack,
).Handle(context.Background(), nil)
return err
}

func injectBuildRequest(s *middleware.Stack) *Request {
r := NewStackRequest()
s.Build.Add(
middleware.BuildMiddlewareFunc(
"injectBuildRequest",
func(ctx context.Context, in middleware.BuildInput, next middleware.BuildHandler) (
middleware.BuildOutput, middleware.Metadata, error,
) {
return next.HandleBuild(ctx, middleware.BuildInput{Request: r})
},
),
middleware.Before,
)
return r.(*Request)
}

func expectHeader(t *testing.T, header http.Header, h, ev string) {
if av := header.Get(h); ev != av {
t.Errorf("expected header '%s: %s', got '%s'", h, ev, av)
}
}

0 comments on commit 9067dec

Please sign in to comment.