这是indexloc提供的服务,不要输入任何密码
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
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ ignore:
- benchmark
- examples
- adapters
- cookie.go
189 changes: 189 additions & 0 deletions cookie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package huma

import (
"fmt"
"net/http"
"net/textproto"
"strings"
)

// ReadCookie reads a single cookie from the request headers by name. If
// multiple cookies with the same name exist, the first is returned.
func ReadCookie(ctx Context, name string) (*http.Cookie, error) {
headers := []string{}
ctx.EachHeader(func(name, value string) {
if strings.EqualFold(name, "cookie") {
headers = append(headers, value)
}
})
for _, c := range readCookies(headers, name) {
return c, nil
}
return nil, fmt.Errorf("%w: %s", http.ErrNoCookie, name)
}

// ReadCookies reads all cookies from the request headers.
func ReadCookies(ctx Context) []*http.Cookie {
headers := []string{}
ctx.EachHeader(func(name, value string) {
if strings.EqualFold(name, "cookie") {
headers = append(headers, value)
}
})
return readCookies(headers, "")
}

// Everything below this line is copied from the Go standard library. None of
// it is exported, so we have to copy it here to use it. 😭

var isTokenTable = [127]bool{
'!': true,
'#': true,
'$': true,
'%': true,
'&': true,
'\'': true,
'*': true,
'+': true,
'-': true,
'.': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'W': true,
'V': true,
'X': true,
'Y': true,
'Z': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'|': true,
'~': true,
}

func isTokenRune(r rune) bool {
i := int(r)
return i < len(isTokenTable) && isTokenTable[i]
}

func isNotToken(r rune) bool {
return !isTokenRune(r)
}

// readCookies parses all "Cookie" values from the header h and
// returns the successfully parsed Cookies.
//
// if filter isn't empty, only cookies of that name are returned.
func readCookies(lines []string, filter string) []*http.Cookie {
// lines := h["Cookie"]
if len(lines) == 0 {
return []*http.Cookie{}
}

cookies := make([]*http.Cookie, 0, len(lines)+strings.Count(lines[0], ";"))
for _, line := range lines {
line = textproto.TrimString(line)

var part string
for len(line) > 0 { // continue since we have rest
part, line, _ = strings.Cut(line, ";")
part = textproto.TrimString(part)
if part == "" {
continue
}
name, val, _ := strings.Cut(part, "=")
name = textproto.TrimString(name)
if !isCookieNameValid(name) {
continue
}
if filter != "" && filter != name {
continue
}
val, ok := parseCookieValue(val, true)
if !ok {
continue
}
cookies = append(cookies, &http.Cookie{Name: name, Value: val})
}
}
return cookies
}

func validCookieValueByte(b byte) bool {
return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\'
}

func parseCookieValue(raw string, allowDoubleQuote bool) (string, bool) {
// Strip the quotes, if present.
if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' {
raw = raw[1 : len(raw)-1]
}
for i := 0; i < len(raw); i++ {
if !validCookieValueByte(raw[i]) {
return "", false
}
}
return raw, true
}

func isCookieNameValid(raw string) bool {
if raw == "" {
return false
}
return strings.IndexFunc(raw, isNotToken) < 0
}
33 changes: 33 additions & 0 deletions docs/docs/features/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,37 @@ func NewHumaAPI() huma.API {
}
```

### Cookies

You can use the `huma.Context` interface along with [`huma.ReadCookie`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#ReadCookie) or [`huma.ReadCookies`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#ReadCookies) to access cookies from middleware, and can also write cookies by adding `Set-Cookie` headers in the response:

```go
func MyMiddleware(ctx huma.Context, next func(huma.Context)) {
// Read a cookie by name.
sessionCookie := huma.ReadCookie(ctx, "session")
fmt.Println(sessionCookie)

// Read all the cookies from the request.
cookies := huma.ReadCookies(ctx)
fmt.Println(cookies)

// Set a cookie in the response. Using `ctx.AppendHeader` won't overwrite
// any existing headers, for example if other middleware might also set
// headers or if this code were moved after the `next` call and the operation
// might set the same header. You can also call `ctx.AppendHeader` multiple
// times to write more than one cookie.
cookie := http.Cookie{
Name: "session",
Value: "123",
}
ctx.AppendHeader("Set-Cookie", cookie.String())

// Call the next middleware in the chain. This eventually calls the
// operation handler as well.
next(ctx)
}
```

### Errors

If your middleware encounters an error, you can stop the processing of the next middleware or operation handler by skipping the call to `next` and writing an error response.
Expand Down Expand Up @@ -103,5 +134,7 @@ func MyMiddleware(ctx huma.Context, next func(ctx huma.Context)) {
- Reference
- [`huma.Context`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Context) a router-agnostic request/response context
- [`huma.Middlewares`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Middlewares) the API instance
- [`huma.ReadCookie`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#ReadCookie) reads a named cookie from a request
- [`huma.ReadCookies`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#ReadCookies) reads cookies from a request
- [`huma.WriteErr`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#WriteErr) function to write error responses
- [`huma.API`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#API) the API instance
11 changes: 11 additions & 0 deletions docs/docs/features/request-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Requests can have parameters and/or a body as input to the handler function. Inp
| `path` | Name of the path parameter | `path:"thing-id"` |
| `query` | Name of the query string parameter | `query:"q"` |
| `header` | Name of the header parameter | `header:"Authorization"` |
| `cookie` | Name of the cookie parameter | `cookie:"session"` |
| `required` | Mark a query/header param as required | `required:"true"` |

!!! info "Required"
Expand All @@ -34,6 +35,16 @@ The following parameter types are supported out of the box:

For example, if the parameter is a query param and the type is `[]string` it might look like `?tags=tag1,tag2` in the URI.

For cookies, the default behavior is to read the cookie _value_ from the request and convert it to one of the types above. If you want to access the entire cookie, you can use `http.Cookie` as the type instead:

```go title="code.go"
type MyInput struct {
Session http.Cookie `cookie:"session"`
}
```

Then you can access e.g. `input.Session.Name` or `input.Session.Value`.

## Request Body

The special struct field `Body` will be treated as the input request body and can refer to any other type or you can embed a struct or slice inline. If the body is a pointer, then it is optional. All doc & validation tags are allowed on the body in addition to these tags:
Expand Down
49 changes: 46 additions & 3 deletions docs/docs/features/response-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ huma.Register(api, huma.Operation{

Headers are set by fields on the response struct. Here are the available tags:

| Tag | Description | Example |
| -------- | --------------------------- | ------------------------ |
| `header` | Name of the response header | `header:"Authorization"` |
| Tag | Description | Example |
| ------------ | --------------------------- | -------------------------------------------- |
| `header` | Name of the response header | `header:"Authorization"` |
| `timeFormat` | Format of a `time.Time` | `timeFormat:"Mon, 02 Jan 2006 15:04:05 GMT"` |

Here's an example of a response with several headers of different types:

Expand All @@ -77,6 +78,48 @@ type MyOutput struct {
}
```

If the field type implements the [`fmt.Stringer`](https://pkg.go.dev/fmt#Stringer) interface then that will be used to convert the value to a string.

### Set vs. Append

By default, headers are set on the response, which overwrites any existing header of the same name. If you want to append to an existing header, you can use an array of values instead of a single value.

```go title="code.go"
type MyOutput struct {
MyHeader []string `header:"My-Header"`
}
```

If you want to append just one header, you can use a slice with a single value.

### Cookies

You can set cookies in the response by using the [`Set-Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) header. The [`http.Cookie`](https://pkg.go.dev/net/http#Cookie) type can be used to represent the cookie without needing to manually convert it to a string.

```go title="code.go"
type MyOutput struct {
SetCookie http.Cookie `header:"Set-Cookie"`
}

huma.Register(api, huma.Operation{
OperationID: "set-cookie",
Method: http.MethodGet,
Path: "/set-cookie",
Summary: "Set a cookie",
}, func(ctx context.Context, *struct{}) (*MyOutput, error) {
// Create a response and set the cookie
resp := &MyOutput{
SetCookie: http.Cookie{
Name: "session",
Value: "123",
},
}
return resp, nil
}
```

You can set multiple cookies by using a slice like `[]http.Cookie` instead.

## Body

The special struct field `Body` will be treated as the response body and can refer to any other type or you can embed a struct or slice inline. A default `Content-Type` header will be set if none is present, selected via client-driven content negotiation with the server based on the registered serialization types.
Expand Down
Loading