pax_global_header00006660000000000000000000000064152023231000014476gustar00rootroot0000000000000052 comment=423c407791a74d4353830e07d98375a0c5cf311b go-openapi-runtime-decad8f/000077500000000000000000000000001520232310000160555ustar00rootroot00000000000000go-openapi-runtime-decad8f/.claude/000077500000000000000000000000001520232310000173705ustar00rootroot00000000000000go-openapi-runtime-decad8f/.claude/.gitignore000066400000000000000000000000501520232310000213530ustar00rootroot00000000000000plans/ skills/ commands/ agents/ hooks/ go-openapi-runtime-decad8f/.claude/CLAUDE.md000066400000000000000000000133571520232310000206600ustar00rootroot00000000000000# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview `github.com/go-openapi/runtime` is the runtime library for the go-openapi toolkit. It provides HTTP client and server components used by code generated by [go-swagger](https://github.com/go-swagger/go-swagger) and for untyped OpenAPI/Swagger API usage. See [docs/MAINTAINERS.md](../docs/MAINTAINERS.md) for CI/CD, release process, and repo structure details. ### Modules and Package layout This is a mono-repo with a `go.work` workspace containing three modules: | Module | Purpose | |--------|---------| | `.` (root) | Core runtime library | | `server-middleware` | Standalone, dependency-free server middleware: `mediatype`, `negotiate`, `negotiate/header`, `docui`. No transitive dependency on `go-openapi/spec`, `loads`, or `validate`. | | `client-middleware/opentracing` | Optional OpenTracing middleware for client transport (compatibility module; new code should use the OpenTelemetry support built into `client.Runtime`). | Packages in the root module: | Package | Contents | |---------|----------| | `runtime` (root) | Core interfaces (`Consumer`, `Producer`, `Authenticator`, `Authorizer`, `OperationHandler`), content-type handlers (JSON, XML, CSV, text, bytestream), HTTP request/response helpers | | `client` | HTTP client transport: `Runtime` with TLS, timeouts, proxy, keepalive, OpenTelemetry tracing | | `middleware` | Server-side request lifecycle: routing, parameter binding, validation, security, operation execution. Doc-UI and negotiation primitives have moved to `server-middleware/*`; the legacy entry points (`Spec`, `SwaggerUI`, `RapiDoc`, `Redoc`, `NegotiateContentType`, …) remain as deprecated shims in `seam.go`. | | `middleware/denco` | Internal path-pattern router | | `middleware/header` | Deprecated shim — re-exports `server-middleware/negotiate/header` | | `middleware/untyped` | Untyped (reflection-based) API request/response handling | | `security` | Auth scheme implementations: Basic, API Key, Bearer/OAuth2 (with `*Ctx` context-aware variants) | | `logger` | `Logger` interface with `Printf`/`Debugf`; debug enabled via `SWAGGER_DEBUG` or `DEBUG` env vars | | `flagext` | CLI flag extensions (e.g. `ByteSize`) | | `yamlpc` | YAML producer/consumer support | | `internal/testing` | Internal test utilities | ### Key API Core interfaces (root package, `interfaces.go`): - `Consumer` / `ConsumerFunc` — bind request body to a Go value - `Producer` / `ProducerFunc` — serialize a Go value to the response body - `Authenticator` / `AuthenticatorFunc` — authenticate a request, return a principal - `Authorizer` / `AuthorizerFunc` — authorize a principal for a request - `OperationHandler` / `OperationHandlerFunc` — handle a matched API operation - `Validatable`, `ContextValidatable` — custom validation hooks for generated types Built-in content-type factories: `JSONConsumer()`, `JSONProducer()`, `XMLConsumer()`, `XMLProducer()`, `CSVConsumer()`, `CSVProducer()`, `TextConsumer()`, `TextProducer()`, `ByteStreamConsumer()`, `ByteStreamProducer()` Client transport (`client` package): - `Runtime` — configurable HTTP transport (TLS, auth, timeout, OpenTelemetry) - `TLSClientOptions` — mTLS / custom CA configuration Server middleware (`middleware` package): - `Context` — request lifecycle manager (routes, binds, validates, authenticates, executes) - `NewRouter()` — builds a router from an analyzed OpenAPI spec Standalone server middleware (`server-middleware` module): - `mediatype.MediaType`, `mediatype.Set` — typed RFC 7231 media-type values + asymmetric matching - `negotiate.ContentType()`, `negotiate.ContentEncoding()` — `Accept` / `Accept-Encoding` selection (honours MIME parameters by default; opt out with `WithIgnoreParameters`) - `docui.SwaggerUI()`, `docui.RapiDoc()`, `docui.Redoc()`, `docui.ServeSpec()` — stdlib-only doc-UI and spec-serving handlers ### Dependencies Key direct dependencies (`go.mod`): | Dependency | Role | |------------|------| | `go-openapi/analysis` | OpenAPI spec analysis | | `go-openapi/errors` | Structured API error types | | `go-openapi/loads` | OpenAPI spec loading | | `go-openapi/spec` | OpenAPI spec model types | | `go-openapi/strfmt` | String format registry (date-time, UUID, etc.) | | `go-openapi/swag/*` | Utilities: conv, fileutils, jsonutils, stringutils | | `go-openapi/validate` | Spec-based validation | | `go-openapi/testify/v2` | Test assertions (zero-dep fork of stretchr/testify) | | `go.opentelemetry.io/otel` | OpenTelemetry tracing | | `docker/go-units` | Human-readable size parsing | ### Notable historical design decisions - **Functional adapter pattern**: every core interface has a companion `*Func` type (e.g. `ConsumerFunc`) so handlers can be plain functions or full structs. - **Consumer/Producer split**: request deserialization and response serialization are decoupled behind separate interfaces, allowing per-content-type pluggability. - **Context-aware auth variants**: security functions come in pairs (`BasicAuth` / `BasicAuthCtx`) to support both legacy and context-propagating call paths. - **Middleware pipeline**: server processing flows through Router → Binder → Validator → Security → OperationExecutor → Responder, each stage composable via `middleware.Builder`. - **OpenTracing kept in a separate module**: avoids pulling the OpenTracing dependency into consumers that only need the core runtime or use OpenTelemetry directly. - **`server-middleware` extracted as a separate module**: lets any `net/http` application reuse the negotiation, media-type, and doc-UI primitives without inheriting the OpenAPI spec/loads/validate dependency tree. The root `middleware` package keeps deprecated shims for source compatibility. go-openapi-runtime-decad8f/.claude/rules/000077500000000000000000000000001520232310000205225ustar00rootroot00000000000000go-openapi-runtime-decad8f/.claude/rules/contributions.md000066400000000000000000000026151520232310000237520ustar00rootroot00000000000000--- paths: - "**/*" --- # Contribution rules (go-openapi) Read `.github/CONTRIBUTING.md` before opening a pull request. ## Commit hygiene - Every commit **must** be DCO signed-off (`git commit -s`) with a real email address. PGP-signed commits are appreciated but not required. - Agents may be listed as co-authors (`Co-Authored-By:`) but the commit **author must be the human sponsor**. We do not accept commits solely authored by bots or agents. - Squash commits into logical units of work before requesting review (`git rebase -i`). ## Linting Before pushing, verify your changes pass linting against the base branch: ```sh golangci-lint run --new-from-rev master ``` Install the latest version if you don't have it: ```sh go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest ``` ## Problem statement - Clearly describe the problem the PR solves, or reference an existing issue. - PR descriptions must not be vague ("fix bug", "improve code") — explain *what* was wrong and *why* the change is correct. ## Tests are mandatory - Every bug fix or feature **must** include tests that demonstrate the problem and verify the fix. - The only exceptions are documentation changes and typo fixes. - Aim for at least 80% coverage of your patch. - Run the full test suite before submitting: For mono-repos: ```sh go test work ./... ``` For single module repos: ```sh go test ./... ``` go-openapi-runtime-decad8f/.claude/rules/github-workflows-conventions.md000066400000000000000000000206451520232310000267330ustar00rootroot00000000000000--- paths: - ".github/workflows/**.yml" - ".github/workflows/**.yaml" --- # GitHub Actions Workflows Formatting and Style Conventions This rule captures YAML and bash formatting rules to provide a consistent maintainer's experience across CI workflows. ## File Structure **REQUIRED**: All github action workflows are organized as a flat structure beneath `.github/workflows/`. > GitHub does not support a hierarchical organization for workflows yet. **REQUIRED**: YAML files are conventionally named `{workflow}.yml`, with the `.yml` extension. ## Code Style & Formatting ### Expression Spacing **REQUIRED**: All GitHub Actions expressions must have spaces inside the braces: ```yaml # ✅ CORRECT env: PR_URL: ${{ github.event.pull_request.html_url }} TOKEN: ${{ secrets.GITHUB_TOKEN }} # ❌ WRONG env: PR_URL: ${{github.event.pull_request.html_url}} TOKEN: ${{secrets.GITHUB_TOKEN}} ``` > Provides a consistent formatting rule. ### Conditional Syntax **REQUIRED**: Always use `${{ }}` in `if:` conditions: ```yaml # ✅ CORRECT if: ${{ inputs.enable-signing == 'true' }} if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} # ❌ WRONG (works but inconsistent) if: inputs.enable-signing == 'true' ``` > Provides a consistent formatting rule. ### GitHub Workflow Commands **REQUIRED**: Use workflow commands for status messages that should appear as annotations, with **double colon separator**: ```bash # ✅ CORRECT - Double colon (::) separator after title echo "::notice title=build::Build completed successfully" echo "::warning title=race-condition::Merge already in progress" echo "::error title=deployment::Failed to deploy" # ❌ WRONG - Single colon separator (won't render as annotation) echo "::notice title=build:Build completed" # Missing second ':' echo "::warning title=x:message" # Won't display correctly ``` **Syntax pattern:** `::LEVEL title=TITLE::MESSAGE` - `LEVEL`: notice, warning, or error - Double `::` separator is required between title and message > Wrong syntax may raise untidy warnings and produce botched output. ### YAML arrays formatting For steps, YAML arrays are formatted with the following indentation: ```yaml # ✅ CORRECT - Clear spacing between steps steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # ❌ WRONG - Dense format, more difficult to read steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # ❌ WRONG - YAML comment or blank line could be avoided steps: # - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 ``` ## Security Best Practices ### Version Pinning using SHAs **REQUIRED**: Always pin action versions to commit SHAs: > Runs must be repeatable with known pinned version. Automated updates are pushed frequently (e.g. daily or weekly) > to keep pinned versions up-to-date. ```yaml # ✅ CORRECT - Pinned to commit SHA with version comment uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 # ❌ WRONG - Mutable tag reference uses: actions/checkout@v6 ``` ### Permission settings **REQUIRED**: Always set minimal permissions at the workflow level. ```yaml # ✅ CORRECT - Workflow level permissions set to minimum permissions: contents: read # ❌ WRONG - Workflow level permissions with undue privilege escalation permissions: contents: write pull-requests: write ``` **REQUIRED**: Whenever a job needs elevated privileges, always raise required permissions at the job level. ```yaml # ✅ CORRECT - Job level permissions set to the specific requirements for that job jobs: dependabot: permissions: contents: write pull-requests: write uses: ./.github/workflows/auto-merge.yml secrets: inherit # ❌ WRONG - Same permissions but set at workflow level instead of job level permissions: contents: write pull-requests: write ``` > (Security best practice detected by CodeQL analysis) ### Undue secret exposure **NEVER** use `secrets[inputs.name]` — always use explicit secret parameters. > Using keyed access to secrets forces the runner to expose ALL secrets to the job, which causes a security risk > (caught and reported by CodeQL security analysis). ```yaml # ❌ SECURITY VULNERABILITY # This exposes ALL organization and repository secrets to the runner on: workflow_call: inputs: secret-name: type: string jobs: my-job: steps: - uses: some-action@v1 with: token: ${{ secrets[inputs.secret-name] }} # ❌ DANGEROUS! ``` **SOLUTION**: Use explicit secret parameters with fallback for defaults: ```yaml # ✅ SECURE on: workflow_call: secrets: gpg-private-key: required: false jobs: my-job: steps: - uses: go-openapi/gh-actions/ci-jobs/bot-credentials@master with: # Falls back to go-openapi default if not explicitly passed gpg-private-key: ${{ secrets.gpg-private-key || secrets.CI_BOT_GPG_PRIVATE_KEY }} ``` ## Common Gotchas ### Description fields containing parsable expressions **REQUIRED**: **DO NOT** use `${{ }}` expressions in description fields: > They may be parsed by the runner, wrongly interpreted or causing failure (e.g. "not defined in this context"). ```yaml # ❌ WRONG - Can cause YAML parsing errors description: | Pass it as: gpg-private-key: ${{ secrets.MY_KEY }} # ✅ CORRECT description: | Pass it as: secrets.MY_KEY ``` ### Boolean inputs **Boolean inputs are forbidden**: NEVER use `type: boolean` for workflow inputs due to unpredictable type coercion > gh-action expressions using boolean job inputs are hard to predict and come with many quirks. ```yaml # ❌ FORBIDDEN - Boolean inputs have type coercion issues on: workflow_call: inputs: enable-feature: type: boolean # ❌ NEVER USE THIS default: true # The pattern `x == 'true' || x == true` seems safe but fails when: # - x is not a boolean: `x == true` evaluates to true if x != null # - Type coercion is unpredictable and error-prone # ✅ CORRECT - Always use string type for boolean-like inputs on: workflow_call: inputs: enable-feature: type: string # ✅ Use string instead default: 'true' # String value jobs: my-job: # Simple, reliable comparison if: ${{ inputs.enable-feature == 'true' }} # ✅ In bash, this works perfectly (inputs are always strings in bash): if [[ '${{ inputs.enable-feature }}' == 'true' ]]; then echo "Feature enabled" fi ``` **Rule**: Use `type: string` with values `'true'` or `'false'` for all boolean-like workflow inputs. **Note**: Step outputs and bash variables are always strings, so `x == 'true'` works fine for those. ### YAML fold scalars in action inputs **NEVER** use `>` or `>-` (fold scalars) for `with:` input values: > The YAML spec says fold scalars replace newlines with spaces, but the GitHub Actions runner > does not reliably honor this for action inputs. The action receives the literal multi-line string > instead of a single folded line, which breaks flag parsing. ```yaml # ❌ BROKEN - Fold scalar, args received with embedded newlines - uses: goreleaser/goreleaser-action@... with: args: >- release --clean --release-notes /tmp/notes.md # ✅ CORRECT - Single line - uses: goreleaser/goreleaser-action@... with: args: release --clean --release-notes /tmp/notes.md # ✅ CORRECT - Literal block scalar (|) is fine for run: scripts - run: | echo "line 1" echo "line 2" ``` **Rule**: Use single-line strings for `with:` inputs. Only use `|` (literal block scalar) for `run:` scripts where multi-line is intentional. go-openapi-runtime-decad8f/.claude/rules/go-conventions.md000066400000000000000000000004621520232310000240160ustar00rootroot00000000000000--- paths: - "**/*.go" --- # Code conventions (go-openapi) - All files must have SPDX license headers (Apache-2.0). - Go version policy: support the 2 latest stable Go minor versions. - Commits require DCO sign-off (`git commit -s`). - use `golangci-lint fmt` to format code (not `gofmt` or `gofumpt`) go-openapi-runtime-decad8f/.claude/rules/linting.md000066400000000000000000000006251520232310000225130ustar00rootroot00000000000000--- paths: - "**/*.go" --- # Linting conventions (go-openapi) ```sh golangci-lint run ``` Config: `.golangci.yml` — posture is `default: all` with explicit disables. See `docs/STYLE.md` for the rationale behind each disabled linter. Key rules: - Every `//nolint` directive **must** have an inline comment explaining why. - Prefer disabling a linter over scattering `//nolint` across the codebase. go-openapi-runtime-decad8f/.claude/rules/testing.md000066400000000000000000000017121520232310000225220ustar00rootroot00000000000000--- paths: - "**/*_test.go" --- # Testing conventions (go-openapi) ## Running tests **Single module repos:** ```sh go test ./... ``` **Mono-repos (with `go.work`):** ```sh # All modules go test work ./... # Single module go test ./conv/... ``` Note: in mono-repos, plain `go test ./...` only tests the root module. The `work` pattern expands to all modules listed in `go.work`. CI runs tests on `{ubuntu, macos, windows} x {stable, oldstable}` with `-race` via `gotestsum`. ## Fuzz tests ```sh # List all fuzz targets go test -list Fuzz ./... # Run a specific target (go test -fuzz cannot span multiple packages) go test -fuzz=Fuzz -run='FuzzTargetName$' -fuzztime=1m30s ./package ``` Fuzz corpus lives in `testdata/fuzz/` within each package. CI runs each fuzz target for 1m30s with a 5m minimize timeout. ## Test framework `github.com/go-openapi/testify/v2` — a zero-dep fork of `stretchr/testify`. Because it's a fork, `testifylint` does not work. go-openapi-runtime-decad8f/.editorconfig000066400000000000000000000010331520232310000205270ustar00rootroot00000000000000# top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = true # Set default charset [*.{js,py,go,scala,rb,java,html,css,less,sass,md}] charset = utf-8 # Tab indentation (no size specified) [*.go] indent_style = tab [*.md] trim_trailing_whitespace = false # Matches the exact files either package.json or .travis.yml [{package.json,.travis.yml}] indent_style = space indent_size = 2 go-openapi-runtime-decad8f/.gitattributes000066400000000000000000000000211520232310000207410ustar00rootroot00000000000000*.go text eol=lf go-openapi-runtime-decad8f/.github/000077500000000000000000000000001520232310000174155ustar00rootroot00000000000000go-openapi-runtime-decad8f/.github/copilot000077700000000000000000000000001520232310000236072../.claude/rulesustar00rootroot00000000000000go-openapi-runtime-decad8f/.github/copilot-instructions.md000066400000000000000000000077571520232310000241720ustar00rootroot00000000000000# Copilot Instructions ## Project Overview `github.com/go-openapi/runtime` is the runtime library for the go-openapi toolkit. It provides HTTP client and server components used by code generated by go-swagger and for untyped OpenAPI/Swagger API usage. ## Modules This is a mono-repo with a `go.work` workspace: | Module | Purpose | |--------|---------| | `.` (root) | Core runtime library | | `server-middleware` | Standalone, dependency-free server middleware (`mediatype`, `negotiate`, `negotiate/header`, `docui`) | | `client-middleware/opentracing` | OpenTracing middleware for client transport (compatibility module; prefer the OpenTelemetry support built into `client.Runtime`) | ## Package Layout | Package | Contents | |---------|----------| | `runtime` (root) | Core interfaces (`Consumer`, `Producer`, `Authenticator`, `Authorizer`, `OperationHandler`), content-type handlers (JSON, XML, CSV, text, bytestream), HTTP helpers | | `client` | HTTP client transport (`Runtime`) with TLS, timeouts, proxy, keepalive, OpenTelemetry | | `middleware` | Server request lifecycle: routing, parameter binding, validation, security, operation execution. Doc-UI and negotiation primitives moved to `server-middleware/*`; legacy entry points remain as deprecated shims in `seam.go`. | | `middleware/denco` | Internal path-pattern router | | `middleware/header` | Deprecated shim — re-exports `server-middleware/negotiate/header` | | `middleware/untyped` | Untyped (reflection-based) API handling | | `security` | Auth implementations: Basic, API Key, Bearer/OAuth2 (with `*Ctx` variants) | | `logger` | `Logger` interface; debug via `SWAGGER_DEBUG` or `DEBUG` env vars | | `flagext` | CLI flag extensions (e.g. `ByteSize`) | | `yamlpc` | YAML producer/consumer | ## Key API - `Consumer` / `Producer` — request body binding and response serialization - `Authenticator` / `Authorizer` — authentication and authorization strategies - `OperationHandler` — matched API operation handler - Built-in factories: `JSONConsumer()`, `JSONProducer()`, `XMLConsumer()`, `XMLProducer()`, `CSVConsumer()`, `CSVProducer()`, `TextConsumer()`, `TextProducer()`, `ByteStreamConsumer()`, `ByteStreamProducer()` - `client.Runtime` — configurable HTTP transport (TLS, auth, OpenTelemetry) - `middleware.Context` — server request lifecycle manager - `middleware.NewRouter()` — builds a router from an analyzed OpenAPI spec ## Design Decisions - Every core interface has a companion `*Func` adapter type (e.g. `ConsumerFunc`). - Consumer/Producer are separate interfaces for per-content-type pluggability. - Security functions come in pairs (`BasicAuth` / `BasicAuthCtx`) for context propagation. - Server middleware pipeline: Router → Binder → Validator → Security → Executor → Responder. - OpenTracing lives in a separate module to avoid pulling its dependency into the core. - `server-middleware` is a separate module so any `net/http` app can use negotiation, media-type, and doc-UI primitives without inheriting the OpenAPI spec/loads/validate tree. ## Dependencies - `go-openapi/analysis`, `errors`, `loads`, `spec`, `strfmt`, `swag/*`, `validate` — OpenAPI toolkit - `go.opentelemetry.io/otel` — tracing - `docker/go-units` — human-readable size parsing - `go-openapi/testify/v2` — test-only (zero-dep fork of `stretchr/testify`) ## Conventions - All `.go` files must have SPDX license headers (Apache-2.0). - Commits require DCO sign-off (`git commit -s`). - Formatting: `golangci-lint fmt` (not `gofmt` or `gofumpt`). - Linting: `golangci-lint run` — config in `.golangci.yml` (posture: `default: all` with explicit disables). - Every `//nolint` directive **must** have an inline comment explaining why. - Tests: `go test work ./...` (all modules) or `go test ./...` (root only). CI runs with `-race` on `{ubuntu, macos, windows} x {stable, oldstable}`. - Test framework: `github.com/go-openapi/testify/v2` (not `stretchr/testify`). See `.github/copilot/` (symlinked to `.claude/rules/`) for detailed rules on Go conventions, linting, and testing. go-openapi-runtime-decad8f/.github/dependabot.yaml000066400000000000000000000032151520232310000224070ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" day: "friday" open-pull-requests-limit: 2 # <- default is 5 groups: # <- group all github actions updates in a single PR # 1. development-dependencies are auto-merged development-dependencies: patterns: - '*' - package-ecosystem: "gomod" # We define 4 groups of dependencies to regroup update pull requests: # - development (e.g. test dependencies) # - go-openapi updates # - golang.org (e.g. golang.org/x/... packages) # - other dependencies (direct or indirect) # # * All groups are checked once a week and each produce at most 1 PR. # * All dependabot PRs are auto-approved # # Auto-merging policy, when requirements are met: # 1. development-dependencies are auto-merged # 2. golang.org-dependencies are auto-merged # 3. go-openapi patch updates are auto-merged. Minor/major version updates require a manual merge. # 4. other dependencies require a manual merge directories: - "**/*" schedule: interval: "weekly" day: "friday" open-pull-requests-limit: 4 groups: development-dependencies: patterns: - "github.com/stretchr/testify" golang-org-dependencies: patterns: - "golang.org/*" go-openapi-dependencies: patterns: - "github.com/go-openapi/*" other-dependencies: exclude-patterns: - "github.com/go-openapi/*" - "github.com/stretchr/testify" - "golang.org/*" go-openapi-runtime-decad8f/.github/wordlist.txt000066400000000000000000000021301520232310000220210ustar00rootroot00000000000000APIs CLI CSV CSVOpts ClientAuthInfoWriters CodeFactor CodeQL CompositeValidationError ConstantTimeCompare CreateHttpRequestContext DCO Denco Denco's GoDoc IETF IdleTimeout JSON KiB Maintainer's MultipartForm PR's PRs Petstore PostForm RESTCONF RapiDoc ReadCloser ReadTimeout Repo SPDX SpanOptions TCP TLS TODOs TextMapPropagator Triaging UI Unmarshaller Untyped XYZ YAML agentic alice analysed api authorizer basePath basepath behaviour booleans bytesize bytestream canonicalized charset ci codebase codec codecov config contentType crypto databinding defaultOffer dependabot dev developercertificate env filesystem fka github godoc golang golangci gzip hasKey hasValue html http https implementor interoperating jsonpointer keepalive lifecycle linter's linters maintainer's maxLen md mediaType metalinter middleware middlewares monorepo mux muxer oauth openapi opentelemetry opentracing param params parentCtx parser's petstore pflag pluggability pre prepended recognised repos routable semver sexualized stdlib stdlib's structs symlinked uiOptions unescaped untrusted untyped urlencoded validator vuln www xml go-openapi-runtime-decad8f/.github/workflows/000077500000000000000000000000001520232310000214525ustar00rootroot00000000000000go-openapi-runtime-decad8f/.github/workflows/auto-merge.yml000066400000000000000000000004621520232310000242440ustar00rootroot00000000000000name: Dependabot auto-merge permissions: contents: read on: pull_request: jobs: dependabot: permissions: contents: write pull-requests: write uses: go-openapi/ci-workflows/.github/workflows/auto-merge.yml@7982843ba86a9b47f456a44ed94e82f882b50a8d # v0.2.17 secrets: inherit go-openapi-runtime-decad8f/.github/workflows/bump-release.yml000066400000000000000000000020121520232310000245510ustar00rootroot00000000000000name: Bump Release permissions: contents: read on: workflow_dispatch: inputs: bump-type: description: Type of bump (patch, minor, major) type: choice options: - patch - minor - major default: patch required: false tag-message-title: description: Tag message title to prepend to the release notes required: false type: string tag-message-body: description: | Tag message body to prepend to the release notes. (use "|" to replace end of line). required: false type: string jobs: bump-release: permissions: contents: write pull-requests: write uses: go-openapi/ci-workflows/.github/workflows/bump-release-monorepo.yml@7982843ba86a9b47f456a44ed94e82f882b50a8d # v0.2.17 with: bump-type: ${{ inputs.bump-type }} tag-message-title: ${{ inputs.tag-message-title }} tag-message-body: ${{ inputs.tag-message-body }} secrets: inherit go-openapi-runtime-decad8f/.github/workflows/codeql.yml000066400000000000000000000007311520232310000234450ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ "master" ] pull_request: branches: [ "master" ] paths-ignore: # remove this clause if CodeQL is a required check - '**/*.md' schedule: - cron: '39 19 * * 5' permissions: contents: read jobs: codeql: permissions: contents: read security-events: write uses: go-openapi/ci-workflows/.github/workflows/codeql.yml@7982843ba86a9b47f456a44ed94e82f882b50a8d # v0.2.17 secrets: inherit go-openapi-runtime-decad8f/.github/workflows/contributors.yml000066400000000000000000000005301520232310000247300ustar00rootroot00000000000000name: Contributors on: schedule: - cron: '18 4 * * 6' workflow_dispatch: permissions: contents: read jobs: contributors: permissions: pull-requests: write contents: write uses: go-openapi/ci-workflows/.github/workflows/contributors.yml@7982843ba86a9b47f456a44ed94e82f882b50a8d # v0.2.17 secrets: inherit go-openapi-runtime-decad8f/.github/workflows/go-test.yml000066400000000000000000000004361520232310000235620ustar00rootroot00000000000000name: go test permissions: pull-requests: read contents: read on: push: branches: - master pull_request: jobs: test: uses: go-openapi/ci-workflows/.github/workflows/go-test-monorepo.yml@7982843ba86a9b47f456a44ed94e82f882b50a8d # v0.2.17 secrets: inherit go-openapi-runtime-decad8f/.github/workflows/scanner.yml000066400000000000000000000005741520232310000236340ustar00rootroot00000000000000name: Vulnerability scans on: branch_protection_rule: push: branches: ["master"] schedule: - cron: "18 4 * * 3" permissions: contents: read jobs: scanners: permissions: contents: read security-events: write uses: go-openapi/ci-workflows/.github/workflows/scanner.yml@7982843ba86a9b47f456a44ed94e82f882b50a8d # v0.2.17 secrets: inherit go-openapi-runtime-decad8f/.github/workflows/tag-release.yml000066400000000000000000000005751520232310000243750ustar00rootroot00000000000000name: Release on tag permissions: contents: read on: push: tags: - v[0-9]+* jobs: gh-release: name: Create release permissions: contents: write uses: go-openapi/ci-workflows/.github/workflows/release.yml@7982843ba86a9b47f456a44ed94e82f882b50a8d # v0.2.17 with: tag: ${{ github.ref_name }} is-monorepo: true secrets: inherit go-openapi-runtime-decad8f/.github/workflows/update-doc.yml000066400000000000000000000105421520232310000242240ustar00rootroot00000000000000name: "Update documentation" permissions: contents: read on: push: tags: - v* branches: [ "master" ] paths: - docs/** - hack/doc-site/** - .github/workflows/update-doc.yml pull_request: paths: - docs/** - hack/doc-site/** - .github/workflows/update-doc.yml concurrency: group: "pages" cancel-in-progress: false defaults: run: shell: bash jobs: build-doc: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: '1' submodules: recursive sparse-checkout: | hack/ docs/ - name: Get all tags [go-openapi repo] if: ${{ github.repository == 'go-openapi/runtime' }} run: | git fetch origin --prune --update-shallow --tags 'refs/tags/*:refs/tags/*' - name: Get all tags [fork] if: ${{ github.repository != 'go-openapi/runtime' }} run: | git remote add upstream "https://github.com/go-openapi/runtime" git fetch upstream --prune --update-shallow --tags 'refs/tags/*:refs/tags/*' git fetch origin --prune --update-shallow --tags 'refs/tags/*:refs/tags/*' - name: Initialize theme env: RELEARN_VERSION: 9.0.3 run: | cd hack/doc-site/hugo # Clone theme curl -sL -o relearn.tgz https://github.com/McShelby/hugo-theme-relearn/archive/refs/tags/"${RELEARN_VERSION}".tar.gz tar xf relearn.tgz rm -rf themes/hugo-relearn mv "hugo-theme-relearn-${RELEARN_VERSION}" hugo-relearn mv hugo-relearn themes/ - name: Prepare config run: | # Builds a commit-dependant extra config to inject parameterization. # HUGO doesn't support config from the command line. # # Set specific parameters that are used in some parameterized document. # This is used to keep up-to-date installation instructions. cd hack/doc-site/hugo ROOT=$(git rev-parse --show-toplevel) VERSION_MESSAGE="Documentation set for latest master." REQUIRED_GO_VERSION=$(grep "^go\s" "${ROOT}"/go.mod|cut -d" " -f2) LATEST_RELEASE=$(git tag --list --sort -version:refname 'v*' 2>/dev/null | head -1 || echo "dev") BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo " Latest release: ${LATEST_RELEASE}" echo " Go version: ${REQUIRED_GO_VERSION}" echo " Build time: ${BUILD_TIME}" echo " Version message: ${VERSION_MESSAGE}" # Generate dynamic config cat runtime.yaml.template \ | sed "s|{{ GO_VERSION }}|${REQUIRED_GO_VERSION}|g" \ | sed "s|{{ LATEST_RELEASE }}|${LATEST_RELEASE}|g" \ | sed "s|{{ VERSION_MESSAGE }}|${VERSION_MESSAGE}|g" \ | sed "s|{{ BUILD_TIME }}|${BUILD_TIME}|g" \ > runtime.yaml - name: Build site with Hugo uses: crazy-max/ghaction-hugo@d629f74d3e4a9da53050610da35a59863ab9b26c # v3.3.0 with: version: v0.153.3 # <- pin the HUGO version, as they often break things extended: true args: > --config hugo.yaml,runtime.yaml --buildDrafts --cleanDestinationDir --minify --printPathWarnings --ignoreCache --noBuildLock --logLevel info --source ${{ github.workspace }}/hack/doc-site/hugo - name: Upload artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: hack/doc-site/hugo/public deploy-doc: if: ${{ github.event_name != 'pull_request' }} needs: build-doc outputs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 - name: Report URL run: | echo "::notice::Deployed doc site to ${{ steps.deployment.outputs.page_url }}" go-openapi-runtime-decad8f/.gitignore000066400000000000000000000000711520232310000200430ustar00rootroot00000000000000*.out *.cov .idea .env .mcp.json go.work.sum .worktrees/ go-openapi-runtime-decad8f/.golangci.yml000066400000000000000000000035261520232310000204470ustar00rootroot00000000000000version: "2" linters: default: all disable: - depguard - err113 # disabled temporarily: there are just too many issues to address - exhaustruct - funlen - gochecknoglobals - gochecknoinits - gocognit - godot - godox - gomoddirectives # moved to mono-repo, multi-modules, so replace directives are needed - gomodguard - gomodguard_v2 - gosmopolitan - inamedparam - ireturn # this repo adopted a pattern where there are quite many returned interfaces. To be challenged with v2 - musttag - nilerr # nilerr crashes on this repo - nlreturn - noinlineerr - nonamedreturns - paralleltest - recvcheck - testpackage - thelper - tparallel - varnamelen - whitespace - wrapcheck - wsl - wsl_v5 settings: dupl: threshold: 200 goconst: min-len: 2 min-occurrences: 3 cyclop: max-complexity: 25 gocyclo: min-complexity: 25 gocognit: min-complexity: 35 exhaustive: default-signifies-exhaustive: true default-case-required: true lll: line-length: 180 exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - .worktrees - third_party$ - builtin$ - examples$ formatters: enable: - gofmt - goimports settings: # local prefixes regroup imports from these packages goimports: local-prefixes: - github.com/go-openapi exclusions: generated: lax paths: - .worktrees - third_party$ - builtin$ issues: # Maximum issues count per one linter. # Set to 0 to disable. # Default: 50 max-issues-per-linter: 0 # Maximum count of issues with the same text. # Set to 0 to disable. # Default: 3 max-same-issues: 0 go-openapi-runtime-decad8f/AGENTS.md000077700000000000000000000000001520232310000254502.github/copilot-instructions.mdustar00rootroot00000000000000go-openapi-runtime-decad8f/CODE_OF_CONDUCT.md000066400000000000000000000062471520232310000206650ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ go-openapi-runtime-decad8f/CONTRIBUTORS.md000066400000000000000000000144321520232310000203400ustar00rootroot00000000000000# Contributors - Repository: ['go-openapi/runtime'] | Total Contributors | Total Contributions | | --- | --- | | 71 | 542 | | Username | All Time Contribution Count | All Commits | | --- | --- | --- | | @casualjim | 268 | | | @fredbi | 117 | | | @youyuanwu | 19 | | | @josephwoodward | 13 | | | @kenjones-cisco | 12 | | | @GlenDC | 7 | | | @moenning | 6 | | | @mstoykov | 6 | | | @elakito | 6 | | | @ifraixedes | 5 | | | @zeitlinger | 4 | | | @Copilot | 3 | | | @jkawamoto | 3 | | | @stoyanr | 3 | | | @keramix | 2 | | | @Equanox | 2 | | | @ederavilaprado | 2 | | | @nan0tube | 2 | | | @thomdixon | 2 | | | @deborggraever | 2 | | | @MakarandNsd | 2 | | | @Vadskye | 2 | | | @jsilland | 2 | | | @Kunde21 | 2 | | | @bcomnes | 2 | | | @galaxie | 2 | | | @anfernee | 2 | | | @wahabmk | 1 | | | @vearutop | 1 | | | @tschaub | 1 | | | @pytlesk4 | 1 | | | @tgraf | 1 | | | @seanprince | 1 | | | @rodriguise | 1 | | | @petrkotas | 1 | | | @maxatome | 1 | | | @maxkarelov | 1 | | | @tooolbox | 1 | | | @akutz | 1 | | | @yabberyabber | 1 | | | @elv-gilles | 1 | | | @gregmarr | 1 | | | @jwalter1-quest | 1 | | | @s4s7 | 1 | | | @stingshen | 1 | | | @tamalsaha | 1 | | | @tte | 1 | | | @martian4202 | 1 | | | @yan-zhuang | 1 | | | @aleksandr-vin | 1 | | | @azylman | 1 | | | @anasmuhmd | 1 | | | @ArFe | 1 | | | @CodeLingoBot | 1 | | | @dlmiddlecote | 1 | | | @danny-cheung | 1 | | | @calavera | 1 | | | @EdwardBetts | 1 | | | @etsangsplk | 1 | | | @ericzsplk | 1 | | | @faguirre1 | 1 | | | @florindragos | 1 | | | @gbjk | 1 | | | @taisho6339 | 1 | | | @jbowes | 1 | | | @JoakimSoderberg | 1 | | | @robbert229 | 1 | | | @jonathaningram | 1 | | | @KuaaMU | 1 | | | @germanhs | 1 | | | @pracucci | 1 | | _this file was generated by the [Contributors GitHub Action](https://github.com/github-community-projects/contributors)_ go-openapi-runtime-decad8f/LICENSE000066400000000000000000000261361520232310000170720ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. go-openapi-runtime-decad8f/NOTICE000066400000000000000000000037611520232310000167700ustar00rootroot00000000000000Copyright 2015-2025 go-swagger maintainers // SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 This software library, github.com/go-openapi/jsonpointer, includes software developed by the go-swagger and go-openapi maintainers ("go-swagger maintainers"). Licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. This software is copied from, derived from, and inspired by other original software products. It ships with copies of other software which license terms are recalled below. The original software was authored in 2014 by Naoya Inada https://github.com/naoina/denco =========================== // SPDX-FileCopyrightText: Copyright (c) 2014 Naoya Inada // SPDX-License-Identifier: MIT Copyright (c) 2014 Naoya Inada Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-openapi-runtime-decad8f/README.md000066400000000000000000000157521520232310000173460ustar00rootroot00000000000000# runtime [![Tests][test-badge]][test-url] [![Coverage][cov-badge]][cov-url] [![CI vuln scan][vuln-scan-badge]][vuln-scan-url] [![CodeQL][codeql-badge]][codeql-url] [![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url] [![Doc][doc-badge]][doc-url] [![GoDoc][godoc-badge]][godoc-url] [![Discord Channel][discord-badge]][discord-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge] --- A runtime for go OpenAPI toolkit. The runtime component for use in code generation or as untyped usage. ## Announcements [**Complete documentation as github pages**][doc-url] **Changes to the API surface in `v0.30.0`**: * utility package `header` has now moved to `github.com/go-openapi/runtime/server-middleware/negotiate/header` > A shim is provided to support existing programs, with a deprecation notice. **Changes in semantics in `v0.30.0`**: Function `negotiate.NegotiateContentType` (available as an alias for backward compatibility as `middleware.NegotiateContentType` now performs a full match considering MIME parameters. The previous behavior (matching in order of appearance after stripping parameters) may be enabled explicitly with option `negotiate.WithIgnoreParameters`. * **2026-05-07** : exposed UI and Spec middleware as a separate, dependency-free module. > Newly available package: `github.com/go-openapi/runtime/server-middleware/docui` that now holds our > UI and spec serve middleware. > > A shim is available in `github.com/go-openapi/runtime/middleware` to bridge the older UI options to the new ones, > with a deprecation notice. > > Methods that were unduly exported and purely used to manipulate options (e.g. `SwaggerUIOpts.EnsureDefaults`) have been > removed. New options in `docui` should be used instead. > Users may reuse this middleware to serve a Redoc, Rapidoc or SwaggerUI documentation without > importing the complete go-openapi scaffolding. * **2026-05-05** : exposed content negotiation methods as a separate, dependency-free module > Users may reuse these utilities to support content-negotiation without extra dependencies. > > Newly available module: `github.com/go-openapi/runtime/server-middleware` > > Newly available packages: `github.com/go-openapi/runtime/server-middleware/negotiate` and > `github.com/go-openapi/runtime/server-middleware/mediatype`. ## Status API is stable. ## Import this library in your project ```cmd go get github.com/go-openapi/runtime ``` ## Change log See For v0.29.0 release see [release notes](docs/NOTES.md). From that release onwards, changes are tracked in the github release notes. **What coming next?** Moving forward, we want to : * [x] fix a few known issues with some file upload requests (e.g. #286) * [] continue narrowing down the scope of dependencies: * [x] split middleware and other useful utilities as a separate dependency-free module * yaml support in an independent module (v2) * introduce more up-to-date support for opentelemetry as a separate module that evolves independently from the main package (to avoid breaking changes, the existing API will remain maintained, but evolve at a slower pace than opentelemetry). (v2) * [] publish proper documentation and examples ## Licensing This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE). See the license [NOTICE](./NOTICE), which recalls the licensing terms of all the pieces of software on top of which it has been built. ## Other documentation * [FAQ](https://go-openapi.github.io/runtime/tutorials/faq/) · [Media-type selection](https://go-openapi.github.io/runtime/tutorials/media-types/) · [Client keep-alive](https://go-openapi.github.io/runtime/tutorials/keep-alive/) * [All-time contributors](./CONTRIBUTORS.md) * [Contributing guidelines][contributing-doc-site] * [Maintainers documentation][maintainers-doc-site] * [Code style][style-doc-site] ## Cutting a new release Maintainers can cut a new release by either: * running [this workflow](https://github.com/go-openapi/runtime/actions/workflows/bump-release.yml) * or pushing a semver tag * signed tags are preferred * The tag message is prepended to release notes [test-badge]: https://github.com/go-openapi/runtime/actions/workflows/go-test.yml/badge.svg [test-url]: https://github.com/go-openapi/runtime/actions/workflows/go-test.yml [cov-badge]: https://codecov.io/gh/go-openapi/runtime/branch/master/graph/badge.svg [cov-url]: https://codecov.io/gh/go-openapi/runtime [vuln-scan-badge]: https://github.com/go-openapi/runtime/actions/workflows/scanner.yml/badge.svg [vuln-scan-url]: https://github.com/go-openapi/runtime/actions/workflows/scanner.yml [codeql-badge]: https://github.com/go-openapi/runtime/actions/workflows/codeql.yml/badge.svg [codeql-url]: https://github.com/go-openapi/runtime/actions/workflows/codeql.yml [release-badge]: https://badge.fury.io/gh/go-openapi%2Fruntime.svg [release-url]: https://badge.fury.io/gh/go-openapi%2Fruntime [gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/runtime [gocard-url]: https://goreportcard.com/report/github.com/go-openapi/runtime [codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/runtime [codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/runtime [doc-badge]: https://img.shields.io/badge/doc-site-blue?link=https%3A%2F%2Fgo-openapi.github.io%2Fruntime%2F [doc-url]: https://go-openapi.github.io/runtime [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/runtime [godoc-url]: http://pkg.go.dev/github.com/go-openapi/runtime [discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue [discord-url]: https://discord.gg/FfnFYaC3k5 [license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg [license-url]: https://github.com/go-openapi/runtime/?tab=Apache-2.0-1-ov-file#readme [goversion-badge]: https://img.shields.io/github/go-mod/go-version/go-openapi/runtime [goversion-url]: https://github.com/go-openapi/runtime/blob/master/go.mod [top-badge]: https://img.shields.io/github/languages/top/go-openapi/runtime [commits-badge]: https://img.shields.io/github/commits-since/go-openapi/runtime/latest [contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html [maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html [style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html go-openapi-runtime-decad8f/SECURITY.md000066400000000000000000000026031520232310000176470ustar00rootroot00000000000000# Security Policy This policy outlines the commitment and practices of the go-openapi maintainers regarding security. ## Supported Versions | Version | Supported | | ------- | ------------------ | | O.x | :white_check_mark: | ## Vulnerability checks in place This repository uses automated vulnerability scans, at every merged commit and at least once a week. We use: * [`GitHub CodeQL`][codeql-url] * [`trivy`][trivy-url] * [`govulncheck`][govulncheck-url] Reports are centralized in github security reports and visible only to the maintainers. ## Reporting a vulnerability If you become aware of a security vulnerability that affects the current repository, **please report it privately to the maintainers** rather than opening a publicly visible GitHub issue. Please follow the instructions provided by github to [Privately report a security vulnerability][github-guidance-url]. > [!NOTE] > On Github, navigate to the project's "Security" tab then click on "Report a vulnerability". [codeql-url]: https://github.com/github/codeql [trivy-url]: https://trivy.dev/docs/latest/getting-started [govulncheck-url]: https://go.dev/blog/govulncheck [github-guidance-url]: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability go-openapi-runtime-decad8f/authinfo_test.go000066400000000000000000000012301520232310000212540ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "testing" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestAuthInfoWriter(t *testing.T) { const bearerToken = "Bearer the-token-goes-here" hand := ClientAuthInfoWriterFunc(func(r ClientRequest, _ strfmt.Registry) error { return r.SetHeaderParam(HeaderAuthorization, bearerToken) }) tr := new(TestClientRequest) require.NoError(t, hand.AuthenticateRequest(tr, nil)) assert.EqualT(t, bearerToken, tr.Headers.Get(HeaderAuthorization)) } go-openapi-runtime-decad8f/bytestream.go000066400000000000000000000116601520232310000205670ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "bytes" "encoding" "errors" "fmt" "io" "reflect" "github.com/go-openapi/swag/jsonutils" ) func defaultCloser() error { return nil } type byteStreamOpt func(opts *byteStreamOpts) // ClosesStream when the bytestream consumer or producer is finished. func ClosesStream(opts *byteStreamOpts) { opts.Close = true } type byteStreamOpts struct { Close bool } // ByteStreamConsumer creates a consumer for byte streams. // // The consumer consumes from a provided reader into the data passed by reference. // // Supported output underlying types and interfaces, prioritized in this order: // // - [io.ReaderFrom] (for maximum control) // - [io.Writer] (performs [io.Copy]) // - [encoding.BinaryUnmarshaler] // - *string // - *[]byte func ByteStreamConsumer(opts ...byteStreamOpt) Consumer { var vals byteStreamOpts for _, opt := range opts { opt(&vals) } return ConsumerFunc(func(reader io.Reader, data any) error { if reader == nil { return errors.New("ByteStreamConsumer requires a reader") // early exit } if data == nil { return errors.New("nil destination for ByteStreamConsumer") } closer := defaultCloser if vals.Close { if cl, isReaderCloser := reader.(io.Closer); isReaderCloser { closer = cl.Close } } defer func() { _ = closer() }() if readerFrom, isReaderFrom := data.(io.ReaderFrom); isReaderFrom { _, err := readerFrom.ReadFrom(reader) return err } if writer, isDataWriter := data.(io.Writer); isDataWriter { _, err := io.Copy(writer, reader) return err } // buffers input before writing to data var buf bytes.Buffer _, err := buf.ReadFrom(reader) if err != nil { return err } b := buf.Bytes() switch destinationPointer := data.(type) { case encoding.BinaryUnmarshaler: return destinationPointer.UnmarshalBinary(b) case *any: switch (*destinationPointer).(type) { case string: *destinationPointer = string(b) return nil case []byte: *destinationPointer = b return nil } default: // check for the underlying type to be pointer to []byte or string, if ptr := reflect.TypeOf(data); ptr.Kind() != reflect.Pointer { return errors.New("destination must be a pointer") } v := reflect.Indirect(reflect.ValueOf(data)) t := v.Type() switch { case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8: v.SetBytes(b) return nil case t.Kind() == reflect.String: v.SetString(string(b)) return nil } } return fmt.Errorf("%v (%T) is not supported by the ByteStreamConsumer, %s", data, data, "can be resolved by supporting Writer/BinaryUnmarshaler interface") }) } // ByteStreamProducer creates a producer for byte streams. // // The producer takes input data then writes to an output writer (essentially as a pipe). // // Supported input underlying types and interfaces, prioritized in this order: // // - [io.WriterTo] (for maximum control) // - [io.Reader] (performs [io.Copy]). A ReadCloser is closed before exiting. // - [encoding.BinaryMarshaler] // - error (writes as a string) // - []byte // - string // - struct, other slices: writes as JSON. func ByteStreamProducer(opts ...byteStreamOpt) Producer { var vals byteStreamOpts for _, opt := range opts { opt(&vals) } return ProducerFunc(func(writer io.Writer, data any) error { if writer == nil { return errors.New("ByteStreamProducer requires a writer") // early exit } if data == nil { return errors.New("nil data for ByteStreamProducer") } closer := defaultCloser if vals.Close { if cl, isWriterCloser := writer.(io.Closer); isWriterCloser { closer = cl.Close } } defer func() { _ = closer() }() if rc, isDataCloser := data.(io.ReadCloser); isDataCloser { defer rc.Close() } switch origin := data.(type) { case io.WriterTo: _, err := origin.WriteTo(writer) return err case io.Reader: _, err := io.Copy(writer, origin) return err case encoding.BinaryMarshaler: bytes, err := origin.MarshalBinary() if err != nil { return err } _, err = writer.Write(bytes) return err case error: _, err := writer.Write([]byte(origin.Error())) return err default: v := reflect.Indirect(reflect.ValueOf(data)) t := v.Type() switch { case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8: _, err := writer.Write(v.Bytes()) return err case t.Kind() == reflect.String: _, err := writer.Write([]byte(v.String())) return err case t.Kind() == reflect.Struct || t.Kind() == reflect.Slice: b, err := jsonutils.WriteJSON(data) if err != nil { return err } _, err = writer.Write(b) return err } } return fmt.Errorf("%v (%T) is not supported by the ByteStreamProducer, %s", data, data, "can be resolved by supporting Reader/BinaryMarshaler interface") }) } go-openapi-runtime-decad8f/bytestream_test.go000066400000000000000000000313761520232310000216340ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "bytes" "crypto/rand" "errors" "fmt" "io" "sync/atomic" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestByteStreamConsumer(t *testing.T) { const expected = "the data for the stream to be sent over the wire" consumer := ByteStreamConsumer() t.Run("can consume as a ReaderFrom", func(t *testing.T) { var dest = &readerFromDummy{} require.NoError(t, consumer.Consume(bytes.NewBufferString(expected), dest)) assert.EqualT(t, expected, dest.b.String()) }) t.Run("can consume as a Writer", func(t *testing.T) { dest := &closingWriter{} require.NoError(t, consumer.Consume(bytes.NewBufferString(expected), dest)) assert.EqualT(t, expected, dest.String()) }) t.Run("can consume as a string", func(t *testing.T) { var dest string require.NoError(t, consumer.Consume(bytes.NewBufferString(expected), &dest)) assert.EqualT(t, expected, dest) }) t.Run("can consume as a binary unmarshaler", func(t *testing.T) { var dest binaryUnmarshalDummy require.NoError(t, consumer.Consume(bytes.NewBufferString(expected), &dest)) assert.EqualT(t, expected, dest.str) }) t.Run("can consume as a binary slice", func(t *testing.T) { var dest []byte require.NoError(t, consumer.Consume(bytes.NewBufferString(expected), &dest)) assert.EqualT(t, expected, string(dest)) }) t.Run("can consume as a type, with underlying as a binary slice", func(t *testing.T) { type binarySlice []byte var dest binarySlice require.NoError(t, consumer.Consume(bytes.NewBufferString(expected), &dest)) assert.EqualT(t, expected, string(dest)) }) t.Run("can consume as a type, with underlying as a string", func(t *testing.T) { type aliasedString string var dest aliasedString require.NoError(t, consumer.Consume(bytes.NewBufferString(expected), &dest)) assert.EqualT(t, expected, string(dest)) }) t.Run("can consume as an interface with underlying type []byte", func(t *testing.T) { var dest any = []byte{} require.NoError(t, consumer.Consume(bytes.NewBufferString(expected), &dest)) asBytes, ok := dest.([]byte) require.TrueT(t, ok) assert.EqualT(t, expected, string(asBytes)) }) t.Run("can consume as an interface with underlying type string", func(t *testing.T) { var dest any = "x" require.NoError(t, consumer.Consume(bytes.NewBufferString(expected), &dest)) asString, ok := dest.(string) require.TrueT(t, ok) assert.EqualT(t, expected, asString) }) t.Run("with CloseStream option", func(t *testing.T) { t.Run("wants to close stream", func(t *testing.T) { closingConsumer := ByteStreamConsumer(ClosesStream) var dest bytes.Buffer r := &closingReader{b: bytes.NewBufferString(expected)} require.NoError(t, closingConsumer.Consume(r, &dest)) assert.EqualT(t, expected, dest.String()) assert.EqualValues(t, 1, r.calledClose) }) t.Run("don't want to close stream", func(t *testing.T) { nonClosingConsumer := ByteStreamConsumer() var dest bytes.Buffer r := &closingReader{b: bytes.NewBufferString(expected)} require.NoError(t, nonClosingConsumer.Consume(r, &dest)) assert.EqualT(t, expected, dest.String()) assert.EqualValues(t, 0, r.calledClose) }) }) t.Run("error cases", func(t *testing.T) { t.Run("passing in a nil slice will result in an error", func(t *testing.T) { var dest *[]byte require.Error(t, consumer.Consume(bytes.NewBufferString(expected), &dest)) }) t.Run("passing a non-pointer will result in an error", func(t *testing.T) { var dest []byte require.Error(t, consumer.Consume(bytes.NewBufferString(expected), dest)) }) t.Run("passing in nil destination result in an error", func(t *testing.T) { require.Error(t, consumer.Consume(bytes.NewBufferString(expected), nil)) }) t.Run("a reader who results in an error, will make it fail", func(t *testing.T) { t.Run("binaryUnmarshal case", func(t *testing.T) { var dest binaryUnmarshalDummy require.Error(t, consumer.Consume(new(nopReader), &dest)) }) t.Run("[]byte case", func(t *testing.T) { var dest []byte require.Error(t, consumer.Consume(new(nopReader), &dest)) }) }) t.Run("the reader cannot be nil", func(t *testing.T) { var dest []byte require.Error(t, consumer.Consume(nil, &dest)) }) }) } func BenchmarkByteStreamConsumer(b *testing.B) { const bufferSize = 1000 expected := make([]byte, bufferSize) _, err := rand.Read(expected) require.NoError(b, err) consumer := ByteStreamConsumer() input := bytes.NewReader(expected) b.Run("with writer", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() var dest bytes.Buffer for b.Loop() { err = consumer.Consume(input, &dest) if err != nil { b.Fatal(err) } _, _ = input.Seek(0, io.SeekStart) dest.Reset() } }) b.Run("with BinaryUnmarshal", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() var dest binaryUnmarshalDummyZeroAlloc for b.Loop() { err = consumer.Consume(input, &dest) if err != nil { b.Fatal(err) } _, _ = input.Seek(0, io.SeekStart) } }) b.Run("with string", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() var dest string for b.Loop() { err = consumer.Consume(input, &dest) if err != nil { b.Fatal(err) } _, _ = input.Seek(0, io.SeekStart) } }) b.Run("with []byte", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() var dest []byte for b.Loop() { err = consumer.Consume(input, &dest) if err != nil { b.Fatal(err) } _, _ = input.Seek(0, io.SeekStart) } }) b.Run("with aliased string", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() type aliasedString string var dest aliasedString for b.Loop() { err = consumer.Consume(input, &dest) if err != nil { b.Fatal(err) } _, _ = input.Seek(0, io.SeekStart) } }) b.Run("with aliased []byte", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() type binarySlice []byte var dest binarySlice for b.Loop() { err = consumer.Consume(input, &dest) if err != nil { b.Fatal(err) } _, _ = input.Seek(0, io.SeekStart) } }) } func TestByteStreamProducer(t *testing.T) { const expected = "the data for the stream to be sent over the wire" producer := ByteStreamProducer() t.Run("can produce from a WriterTo", func(t *testing.T) { var w bytes.Buffer var data io.WriterTo = bytes.NewBufferString(expected) require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, expected, w.String()) }) t.Run("can produce from a Reader", func(t *testing.T) { var w bytes.Buffer var data io.Reader = bytes.NewBufferString(expected) require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, expected, w.String()) }) t.Run("can produce from a binary marshaler", func(t *testing.T) { var w bytes.Buffer data := &binaryMarshalDummy{str: expected} require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, expected, w.String()) }) t.Run("can produce from a string", func(t *testing.T) { var w bytes.Buffer data := expected require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, expected, w.String()) }) t.Run("can produce from a []byte", func(t *testing.T) { var w bytes.Buffer data := []byte(expected) require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, expected, w.String()) }) t.Run("can produce from an error", func(t *testing.T) { var w bytes.Buffer data := errors.New(expected) require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, expected, w.String()) }) t.Run("can produce from an aliased string", func(t *testing.T) { var w bytes.Buffer type aliasedString string var data aliasedString = expected require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, expected, w.String()) }) t.Run("can produce from an interface with underlying type string", func(t *testing.T) { var w bytes.Buffer var data any = expected require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, expected, w.String()) }) t.Run("can produce from an aliased []byte", func(t *testing.T) { var w bytes.Buffer type binarySlice []byte var data binarySlice = []byte(expected) require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, expected, w.String()) }) t.Run("can produce from an interface with underling type []byte", func(t *testing.T) { var w bytes.Buffer var data any = []byte(expected) require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, expected, w.String()) }) t.Run("can produce JSON from an arbitrary struct", func(t *testing.T) { var w bytes.Buffer type dummy struct { Message string `json:"message,omitempty"` } data := dummy{Message: expected} require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, fmt.Sprintf(`{"message":%q}`, expected), w.String()) }) t.Run("can produce JSON from a pointer to an arbitrary struct", func(t *testing.T) { var w bytes.Buffer type dummy struct { Message string `json:"message,omitempty"` } data := dummy{Message: expected} require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, fmt.Sprintf(`{"message":%q}`, expected), w.String()) }) t.Run("can produce JSON from an arbitrary slice", func(t *testing.T) { var w bytes.Buffer data := []string{expected} require.NoError(t, producer.Produce(&w, data)) assert.EqualT(t, fmt.Sprintf(`[%q]`, expected), w.String()) }) t.Run("with CloseStream option", func(t *testing.T) { t.Run("wants to close stream", func(t *testing.T) { closingProducer := ByteStreamProducer(ClosesStream) w := &closingWriter{} data := bytes.NewBufferString(expected) require.NoError(t, closingProducer.Produce(w, data)) assert.EqualT(t, expected, w.String()) assert.EqualValues(t, 1, w.calledClose) }) t.Run("don't want to close stream", func(t *testing.T) { nonClosingProducer := ByteStreamProducer() w := &closingWriter{} data := bytes.NewBufferString(expected) require.NoError(t, nonClosingProducer.Produce(w, data)) assert.EqualT(t, expected, w.String()) assert.EqualValues(t, 0, w.calledClose) }) t.Run("always close data reader whenever possible", func(t *testing.T) { nonClosingProducer := ByteStreamProducer() w := &closingWriter{} data := &closingReader{b: bytes.NewBufferString(expected)} require.NoError(t, nonClosingProducer.Produce(w, data)) assert.EqualT(t, expected, w.String()) assert.EqualValuesf(t, 0, w.calledClose, "expected the input reader NOT to be closed") assert.EqualValuesf(t, 1, data.calledClose, "expected the data reader to be closed") }) }) t.Run("error cases", func(t *testing.T) { t.Run("MarshalBinary error gets propagated", func(t *testing.T) { var writer bytes.Buffer data := new(binaryMarshalDummy) require.Error(t, producer.Produce(&writer, data)) }) t.Run("nil data is never accepted", func(t *testing.T) { var writer bytes.Buffer require.Error(t, producer.Produce(&writer, nil)) }) t.Run("nil writer should also never be acccepted", func(t *testing.T) { data := expected require.Error(t, producer.Produce(nil, data)) }) t.Run("bool is an unsupported type", func(t *testing.T) { var writer bytes.Buffer data := true require.Error(t, producer.Produce(&writer, data)) }) t.Run("WriteJSON error gets propagated", func(t *testing.T) { var writer bytes.Buffer type cannotMarshal struct { X func() `json:"x"` } data := cannotMarshal{} require.Error(t, producer.Produce(&writer, data)) }) }) } type binaryUnmarshalDummy struct { err error str string } type binaryUnmarshalDummyZeroAlloc struct { b []byte } func (b *binaryUnmarshalDummy) UnmarshalBinary(data []byte) error { if b.err != nil { return b.err } if len(data) == 0 { return errors.New("no text given") } b.str = string(data) return nil } func (b *binaryUnmarshalDummyZeroAlloc) UnmarshalBinary(data []byte) error { if len(data) == 0 { return errors.New("no text given") } b.b = data return nil } type binaryMarshalDummy struct { str string } func (b *binaryMarshalDummy) MarshalBinary() ([]byte, error) { if len(b.str) == 0 { return nil, errors.New("no text set") } return []byte(b.str), nil } type closingWriter struct { calledClose int64 calledWrite atomic.Int64 b bytes.Buffer } func (c *closingWriter) Close() error { atomic.AddInt64(&c.calledClose, 1) return nil } func (c *closingWriter) Write(p []byte) (n int, err error) { c.calledWrite.Add(1) return c.b.Write(p) } func (c *closingWriter) String() string { return c.b.String() } type closingReader struct { calledClose int64 calledRead atomic.Int64 b *bytes.Buffer } func (c *closingReader) Close() error { atomic.AddInt64(&c.calledClose, 1) return nil } func (c *closingReader) Read(p []byte) (n int, err error) { c.calledRead.Add(1) return c.b.Read(p) } go-openapi-runtime-decad8f/client-middleware/000077500000000000000000000000001520232310000214465ustar00rootroot00000000000000go-openapi-runtime-decad8f/client-middleware/opentracing/000077500000000000000000000000001520232310000237575ustar00rootroot00000000000000go-openapi-runtime-decad8f/client-middleware/opentracing/README.md000066400000000000000000000046231520232310000252430ustar00rootroot00000000000000# client-middleware/opentracing [![GoDoc][godoc-badge]][godoc-url] OpenTracing instrumentation for the `go-openapi/runtime` client transport. > **Compatibility module.** This module exists solely to support users who > still depend on the legacy [OpenTracing API][opentracing-url] and have not > yet migrated to OpenTelemetry. New code should use the **OpenTelemetry > tracing built into [`client.Runtime`](../../client)** directly — it > requires no extra wrapper. > > The OpenTracing project has been archived since 2022 in favour of > OpenTelemetry. We expect to keep this module compiling and passing tests, > but it will not gain new features. It is published as a **separate Go module** so that the `opentracing-go` dependency stays out of the main runtime's import graph — projects on OpenTelemetry pay no cost for it. ## Install ```sh go get github.com/go-openapi/runtime/client-middleware/opentracing ``` ## Usage `WithOpenTracing` wraps a `client.Runtime` and starts a child span for each outgoing operation, provided the operation's `context.Context` already carries a parent span. If no parent span is found in the context, the call is forwarded unchanged. ```go import ( "github.com/go-openapi/runtime/client" otmw "github.com/go-openapi/runtime/client-middleware/opentracing" ) rt := client.New("api.example.com", "/v1", []string{"https"}) transport := otmw.WithOpenTracing(rt) // pass `transport` (a runtime.ClientTransport) to your generated client ``` Per-request tags can be added through the variadic `opentracing.StartSpanOption` arguments: ```go transport := otmw.WithOpenTracing(rt, opentracing.Tag{Key: "service", Value: "billing"}) ``` ## Migrating to OpenTelemetry The main `client.Runtime` already emits OpenTelemetry spans; no wrapper is needed. Users coming from this module typically: 1. Replace the `opentracing-go` `Tracer` setup with an OpenTelemetry `TracerProvider`. 2. Drop the `WithOpenTracing` wrapper — instrument the `client.Runtime` directly. 3. Remove the import of this module. See the [main runtime README](../../README.md) for the OpenTelemetry configuration entry points. ## License [Apache-2.0](../../LICENSE). [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/runtime/client-middleware/opentracing [godoc-url]: https://pkg.go.dev/github.com/go-openapi/runtime/client-middleware/opentracing [opentracing-url]: https://github.com/opentracing/opentracing-go go-openapi-runtime-decad8f/client-middleware/opentracing/doc.go000066400000000000000000000004561520232310000250600ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // Package opentracing supports opentracing instrumentation for the runtime client. // // It is provided for backward-compatibility for users who can't transition to OTEL. package opentracing go-openapi-runtime-decad8f/client-middleware/opentracing/go.mod000066400000000000000000000036741520232310000250770ustar00rootroot00000000000000module github.com/go-openapi/runtime/client-middleware/opentracing require ( github.com/go-openapi/runtime v0.31.0 github.com/go-openapi/strfmt v0.26.2 github.com/go-openapi/testify/v2 v2.5.0 github.com/opentracing/opentracing-go v1.2.0 ) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.25.0 // indirect github.com/go-openapi/errors v0.22.7 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/loads v0.23.3 // indirect github.com/go-openapi/runtime/server-middleware v0.31.0 // indirect github.com/go-openapi/spec v0.22.4 // indirect github.com/go-openapi/swag/conv v0.26.0 // indirect github.com/go-openapi/swag/fileutils v0.26.0 // indirect github.com/go-openapi/swag/jsonname v0.26.0 // indirect github.com/go-openapi/swag/jsonutils v0.26.0 // indirect github.com/go-openapi/swag/loading v0.26.0 // indirect github.com/go-openapi/swag/mangling v0.26.0 // indirect github.com/go-openapi/swag/stringutils v0.26.0 // indirect github.com/go-openapi/swag/typeutils v0.26.0 // indirect github.com/go-openapi/swag/yamlutils v0.26.0 // indirect github.com/go-openapi/validate v0.25.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.54.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.37.0 // indirect ) replace ( github.com/go-openapi/runtime => ../.. github.com/go-openapi/runtime/server-middleware => ../../server-middleware ) go 1.25.0 go-openapi-runtime-decad8f/client-middleware/opentracing/go.sum000066400000000000000000000211041520232310000251100ustar00rootroot00000000000000github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs= github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/strfmt v0.26.2 h1:ysjheCh4i1rmFEo2LanhELDNucNzfWTZhUDKgWWPaFM= github.com/go-openapi/strfmt v0.26.2/go.mod h1:fXh1e449cyUn2NYuz+wb3wARBUdMl7qPEZwX00nqivY= github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= github.com/go-openapi/testify/enable/yaml/v2 v2.5.0 h1:3hZD1fwydvCx/cc1R2uYNQirHqf2s6lqpKV3FcNTURA= github.com/go-openapi/testify/enable/yaml/v2 v2.5.0/go.mod h1:TvDZKBH7ZbMaF3EqH2AwTvNQCmzyZq8K1agRjf1B+Nk= github.com/go-openapi/testify/v2 v2.5.0 h1:UOCr63aAsMIDydZbZGqo5Ev01D4eydItRbekDuZMJLw= github.com/go-openapi/testify/v2 v2.5.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= go-openapi-runtime-decad8f/client-middleware/opentracing/opentracing.go000066400000000000000000000052071520232310000266230ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package opentracing import ( "fmt" "net/http" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" "github.com/opentracing/opentracing-go/log" "github.com/go-openapi/strfmt" "github.com/go-openapi/runtime" ) type tracingTransport struct { transport runtime.ClientTransport host string opts []opentracing.StartSpanOption } func newOpenTracingTransport(transport runtime.ClientTransport, host string, opts []opentracing.StartSpanOption, ) runtime.ClientTransport { return &tracingTransport{ transport: transport, host: host, opts: opts, } } func (t *tracingTransport) Submit(op *runtime.ClientOperation) (any, error) { if op.Context == nil { return t.transport.Submit(op) } params := op.Params reader := op.Reader var span opentracing.Span defer func() { if span != nil { span.Finish() } }() op.Params = runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error { span = createClientSpan(op, req.GetHeaderParams(), t.host, t.opts) return params.WriteToRequest(req, reg) }) op.Reader = runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if span != nil { code := response.Code() ext.HTTPStatusCode.Set(span, uint16(code)) //nolint:gosec // safe to convert regular HTTP codes, no adverse impact other than a garbled trace when converting a code larger than 65535 if code >= http.StatusBadRequest { ext.Error.Set(span, true) } } return reader.ReadResponse(response, consumer) }) submit, err := t.transport.Submit(op) if err != nil && span != nil { ext.Error.Set(span, true) span.LogFields(log.Error(err)) } return submit, err } func createClientSpan(op *runtime.ClientOperation, header http.Header, host string, opts []opentracing.StartSpanOption) opentracing.Span { ctx := op.Context span := opentracing.SpanFromContext(ctx) if span != nil { opts = append(opts, ext.SpanKindRPCClient) span, _ = opentracing.StartSpanFromContextWithTracer( ctx, span.Tracer(), operationName(op), opts...) ext.Component.Set(span, "go-openapi") ext.PeerHostname.Set(span, host) span.SetTag("http.path", op.PathPattern) ext.HTTPMethod.Set(span, op.Method) _ = span.Tracer().Inject( span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(header)) return span } return nil } func operationName(op *runtime.ClientOperation) string { if op.ID != "" { return op.ID } return fmt.Sprintf("%s_%s", op.Method, op.PathPattern) } go-openapi-runtime-decad8f/client-middleware/opentracing/opentracing_test.go000066400000000000000000000076361520232310000276720ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package opentracing import ( "bytes" "context" "io" "testing" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" "github.com/opentracing/opentracing-go/mocktracer" ) type tres struct { } func (r tres) Code() int { return 490 } func (r tres) Message() string { return "the message" } func (r tres) GetHeader(_ string) string { return "the header" } func (r tres) GetHeaders(_ string) []string { return []string{"the headers", "the headers2"} } func (r tres) Body() io.ReadCloser { return io.NopCloser(bytes.NewBufferString("the content")) } type mockRuntime struct { req runtime.TestClientRequest } func (m *mockRuntime) Submit(operation *runtime.ClientOperation) (any, error) { _ = operation.Params.WriteToRequest(&m.req, nil) _, _ = operation.Reader.ReadResponse(&tres{}, nil) return map[string]any{}, nil } func testOperation(ctx context.Context) *runtime.ClientOperation { return &runtime.ClientOperation{ ID: "getCluster", Method: "GET", PathPattern: "/kubernetes-clusters/{cluster_id}", ProducesMediaTypes: []string{"application/json"}, ConsumesMediaTypes: []string{"application/json"}, Schemes: []string{"https"}, Reader: runtime.ClientResponseReaderFunc(func(runtime.ClientResponse, runtime.Consumer) (any, error) { return nil, nil }), Params: runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }), AuthInfo: client.PassThroughAuth, Context: ctx, } } func Test_TracingRuntime_submit(t *testing.T) { t.Parallel() tracer := mocktracer.New() _, ctx := opentracing.StartSpanFromContextWithTracer(context.Background(), tracer, "op") testSubmit(t, testOperation(ctx), tracer, 1) } func Test_TracingRuntime_submit_nilAuthInfo(t *testing.T) { t.Parallel() tracer := mocktracer.New() _, ctx := opentracing.StartSpanFromContextWithTracer(context.Background(), tracer, "op") operation := testOperation(ctx) operation.AuthInfo = nil testSubmit(t, operation, tracer, 1) } func Test_TracingRuntime_submit_nilContext(t *testing.T) { t.Parallel() tracer := mocktracer.New() _, ctx := opentracing.StartSpanFromContextWithTracer(context.Background(), tracer, "op") operation := testOperation(ctx) operation.Context = nil testSubmit(t, operation, tracer, 0) // just don't panic } func testSubmit(t *testing.T, operation *runtime.ClientOperation, tracer *mocktracer.MockTracer, expectedSpans int) { header := map[string][]string{} r := newOpenTracingTransport(&mockRuntime{runtime.TestClientRequest{Headers: header}}, "remote_host", []opentracing.StartSpanOption{opentracing.Tag{ Key: string(ext.PeerService), Value: "service", }}) _, err := r.Submit(operation) require.NoError(t, err) assert.Len(t, tracer.FinishedSpans(), expectedSpans) if expectedSpans == 1 { span := tracer.FinishedSpans()[0] assert.EqualT(t, "getCluster", span.OperationName) assert.Equal(t, map[string]any{ "component": "go-openapi", "http.method": "GET", "http.path": "/kubernetes-clusters/{cluster_id}", "http.status_code": uint16(490), "peer.hostname": "remote_host", "peer.service": "service", "span.kind": ext.SpanKindRPCClientEnum, "error": true, }, span.Tags()) } } func Test_injectSpanContext(t *testing.T) { t.Parallel() tracer := mocktracer.New() _, ctx := opentracing.StartSpanFromContextWithTracer(context.Background(), tracer, "op") header := map[string][]string{} createClientSpan(testOperation(ctx), header, "", nil) // values are random - just check that something was injected assert.Len(t, header, 3) } go-openapi-runtime-decad8f/client-middleware/opentracing/runtime.go000066400000000000000000000016141520232310000257730ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package opentracing import ( "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" opentracing "github.com/opentracing/opentracing-go" ) // WithOpenTracing adds opentracing support to the provided runtime. // A new client span is created for each request. // // If the context of the client operation does not contain an active span, no span is created. // The provided opts are applied to each spans - for example to add global tags. // // This method is provided to continue supporting users of [github.com/go-openapi/runtime] who // still rely on opentracing and have not been able to transition to opentelemetry yet. func WithOpenTracing(r *client.Runtime, opts ...opentracing.StartSpanOption) runtime.ClientTransport { return newOpenTracingTransport(r, r.Host, opts) } go-openapi-runtime-decad8f/client/000077500000000000000000000000001520232310000173335ustar00rootroot00000000000000go-openapi-runtime-decad8f/client/auth_info.go000066400000000000000000000041011520232310000216320ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "encoding/base64" "github.com/go-openapi/strfmt" "github.com/go-openapi/runtime" ) // PassThroughAuth never manipulates the request. var PassThroughAuth runtime.ClientAuthInfoWriter func init() { PassThroughAuth = runtime.ClientAuthInfoWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) } // BasicAuth provides a basic auth info writer. func BasicAuth(username, password string) runtime.ClientAuthInfoWriter { return runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { encoded := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) return r.SetHeaderParam(runtime.HeaderAuthorization, "Basic "+encoded) }) } // APIKeyAuth provides an API key auth info writer. func APIKeyAuth(name, in, value string) runtime.ClientAuthInfoWriter { if in == "query" { return runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { return r.SetQueryParam(name, value) }) } if in == "header" { return runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { return r.SetHeaderParam(name, value) }) } return nil } // BearerToken provides a header based oauth2 bearer access token auth info writer. func BearerToken(token string) runtime.ClientAuthInfoWriter { return runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { return r.SetHeaderParam(runtime.HeaderAuthorization, "Bearer "+token) }) } // Compose combines multiple ClientAuthInfoWriters into a single one. // Useful when multiple auth headers are needed. func Compose(auths ...runtime.ClientAuthInfoWriter) runtime.ClientAuthInfoWriter { return runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { for _, auth := range auths { if auth == nil { continue } if err := auth.AuthenticateRequest(r, nil); err != nil { return err } } return nil }) } go-openapi-runtime-decad8f/client/auth_info_test.go000066400000000000000000000041341520232310000226770ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "net/http" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client/internal/request" ) func TestBasicAuth(t *testing.T) { r := request.New(http.MethodGet, "/", nil) writer := BasicAuth("someone", "with a password") err := writer.AuthenticateRequest(r, nil) require.NoError(t, err) req := new(http.Request) req.Header = make(http.Header) req.Header.Set(runtime.HeaderAuthorization, r.GetHeaderParams().Get(runtime.HeaderAuthorization)) usr, pw, ok := req.BasicAuth() require.TrueT(t, ok) assert.EqualT(t, "someone", usr) assert.EqualT(t, "with a password", pw) } func TestAPIKeyAuth_Query(t *testing.T) { r := request.New(http.MethodGet, "/", nil) writer := APIKeyAuth("api_key", "query", "the-shared-key") err := writer.AuthenticateRequest(r, nil) require.NoError(t, err) assert.EqualT(t, "the-shared-key", r.GetQueryParams().Get("api_key")) } func TestAPIKeyAuth_Header(t *testing.T) { r := request.New(http.MethodGet, "/", nil) writer := APIKeyAuth("X-Api-Token", "header", "the-shared-key") err := writer.AuthenticateRequest(r, nil) require.NoError(t, err) assert.EqualT(t, "the-shared-key", r.GetHeaderParams().Get("X-Api-Token")) } func TestBearerTokenAuth(t *testing.T) { r := request.New(http.MethodGet, "/", nil) writer := BearerToken("the-shared-token") err := writer.AuthenticateRequest(r, nil) require.NoError(t, err) assert.EqualT(t, "Bearer the-shared-token", r.GetHeaderParams().Get(runtime.HeaderAuthorization)) } func TestCompose(t *testing.T) { r := request.New(http.MethodGet, "/", nil) writer := Compose(APIKeyAuth("X-Api-Key", "header", "the-api-key"), APIKeyAuth("X-Secret-Key", "header", "the-secret-key")) err := writer.AuthenticateRequest(r, nil) require.NoError(t, err) assert.EqualT(t, "the-api-key", r.GetHeaderParams().Get("X-Api-Key")) assert.EqualT(t, "the-secret-key", r.GetHeaderParams().Get("X-Secret-Key")) } go-openapi-runtime-decad8f/client/consts_test.go000066400000000000000000000005341520232310000222340ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client // Test-only constants shared across the package's *_test.go files, // extracted to satisfy the goconst linter. const ( operationID = "getTasks" taskOneContent = "task 1 content" taskTwoContent = "task 2 content" ) go-openapi-runtime-decad8f/client/content_negotiation_test.go000066400000000000000000000673341520232310000250100ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "bytes" "context" "errors" "io" "iter" "net/http" "slices" "strings" "testing" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client/internal/request" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) // This file is a behavioural harness for the client-side content-type // selection paths. It is intentionally exhaustive: each case captures // what the runtime does today (correct behaviour and known bugs alike) // so subsequent fixes for issues #386 and #387 produce visible deltas. // // Cases tagged with #386 lock in behaviour that is still known // to be incorrect — they will be flipped when the picker becomes // payload-aware. const ( ndjsonMime = "application/x-ndjson" vendorMime = "application/x-vendor" vendorMime1 = "application/x-vendor1" vendorMime2 = "application/x-vendor2" ) func TestBuildHTTP_ContentNegotiation(t *testing.T) { runBuildHTTPCases(t, payloadStructCases()) runBuildHTTPCases(t, payloadReaderCases()) runBuildHTTPCases(t, payloadByteSliceCases()) runBuildHTTPCases(t, formFieldCases()) runBuildHTTPCases(t, fileFieldCases()) runBuildHTTPCases(t, formAndFileFieldCases()) runBuildHTTPCases(t, noBodyCases()) runBuildHTTPCases(t, missingProducerCases()) } func TestSubmit_ContentNegotiation(t *testing.T) { runSubmitCases(t, submitWiringCases()) } // buildHTTPCase exercises (*request).buildHTTP directly: the picker // has already chosen mediaType. // // Set wantContentType for an exact match. For multipart cases (where // the boundary is random), set wantContentTypePrefix instead. type buildHTTPCase struct { name string mediaType string // already-picked mime consumes []string // candidate list visible to buildHTTP (Stage-2 input) method string // default POST when empty writer runtime.ClientRequestWriter // SetBodyParam / SetFormParam / SetFileParam producers map[string]runtime.Producer // nil → defaults from New() wantContentType string // exact match; empty → no header expected unless prefix is set wantContentTypePrefix string // prefix match (use for multipart with random boundary) wantBody func(t *testing.T, body []byte) // nil → no body assertion wantErr string // substring of error } // submitCase exercises Runtime.Submit end-to-end via a captured // RoundTripper: both the picker and buildHTTP run. type submitCase struct { name string consumes []string // operation.ConsumesMediaTypes producesAccept []string // operation.ProducesMediaTypes (drives Accept header) method string // default POST when empty writer runtime.ClientRequestWriter producers map[string]runtime.Producer // nil → defaults wantContentType string wantContentTypePrefix string wantBody func(t *testing.T, body []byte) wantErr string } func runBuildHTTPCases(t *testing.T, cases iter.Seq[buildHTTPCase]) { t.Helper() for tc := range cases { t.Run(tc.name, runBuildHTTPCase(tc)) } } func runBuildHTTPCase(tc buildHTTPCase) func(*testing.T) { return func(t *testing.T) { ctx := t.Context() method := tc.method if method == "" { method = http.MethodPost } writer := tc.writer if writer == nil { writer = noopWriter() } producers := tc.producers if producers == nil { producers = New("example.com", "/", []string{schemeHTTP}).Producers } r := request.New(method, "/", writer) r.SetConsumes(tc.consumes) req, cancel, err := r.BuildHTTPContext(ctx, tc.mediaType, "/", producers, strfmt.Default, nil) defer cancel() if tc.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tc.wantErr) return } require.NoError(t, err) got := req.Header.Get(runtime.HeaderContentType) assertContentType(t, tc.wantContentType, tc.wantContentTypePrefix, got) if tc.wantBody != nil { body := readBody(t, req) tc.wantBody(t, body) } } } func runSubmitCases(t *testing.T, cases iter.Seq[submitCase]) { t.Helper() for tc := range cases { t.Run(tc.name, runSubmitCase(tc)) } } func runSubmitCase(tc submitCase) func(*testing.T) { return func(t *testing.T) { method := tc.method if method == "" { method = http.MethodPost } writer := tc.writer if writer == nil { writer = noopWriter() } var captured *http.Request var capturedBody []byte transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) { captured = req if req.Body != nil { b, _ := io.ReadAll(req.Body) capturedBody = b } return &http.Response{ StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(nil)), Request: req, }, nil }) rt := New("example.com", "/", []string{schemeHTTP}) rt.Transport = transport if tc.producers != nil { rt.Producers = tc.producers } _, err := rt.Submit(&runtime.ClientOperation{ ID: "test", Method: method, PathPattern: "/", ProducesMediaTypes: tc.producesAccept, ConsumesMediaTypes: tc.consumes, Params: writer, Reader: runtime.ClientResponseReaderFunc(func(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return nil, nil }), Context: context.Background(), }) if tc.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tc.wantErr) return } require.NoError(t, err) require.NotNil(t, captured, "RoundTripper not invoked") got := captured.Header.Get(runtime.HeaderContentType) assertContentType(t, tc.wantContentType, tc.wantContentTypePrefix, got) if tc.wantBody != nil { tc.wantBody(t, capturedBody) } } } // assertContentType applies whichever Content-Type expectation the case // declares: exact, prefix (for multipart), or "no header expected" when // both are empty. func assertContentType(t *testing.T, wantExact, wantPrefix, got string) { t.Helper() if wantPrefix != "" { require.True(t, strings.HasPrefix(got, wantPrefix), "want Content-Type prefix %q, got %q", wantPrefix, got) return } assert.EqualT(t, wantExact, got) } // readBody reads req.Body fully into a buffer, restoring it as a fresh // reader so the caller can re-read if needed. func readBody(t *testing.T, req *http.Request) []byte { t.Helper() if req.Body == nil { return nil } b, err := io.ReadAll(req.Body) require.NoError(t, err) req.Body = io.NopCloser(bytes.NewReader(b)) return b } // --- writer helpers -------------------------------------------------- func noopWriter() runtime.ClientRequestWriter { return runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) } func writerSetBody(payload any) runtime.ClientRequestWriter { return runtime.ClientRequestWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { return r.SetBodyParam(payload) }) } func writerSetContentType(value string) runtime.ClientRequestWriter { return runtime.ClientRequestWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { return r.SetHeaderParam("Content-Type", value) }) } func writerSetForm(name string, values ...string) runtime.ClientRequestWriter { return runtime.ClientRequestWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { return r.SetFormParam(name, values...) }) } func writerSetFileToUpload(files ...runtime.NamedReadCloser) runtime.ClientRequestWriter { return runtime.ClientRequestWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { return r.SetFileParam("upload", files...) }) } func writerCompose(writers ...runtime.ClientRequestWriter) runtime.ClientRequestWriter { return runtime.ClientRequestWriterFunc(func(r runtime.ClientRequest, reg strfmt.Registry) error { for _, w := range writers { if err := w.WriteToRequest(r, reg); err != nil { return err } } return nil }) } // readerWithCT wraps an io.Reader and declares its MIME type via the // `ContentType() string` opt-in interface that buildHTTP consults for // stream payloads. type readerWithCT struct { io.Reader ct string } func (r *readerWithCT) ContentType() string { return r.ct } // readCloserWithCT is the ReadCloser counterpart. type readCloserWithCT struct { io.ReadCloser ct string } func (r *readCloserWithCT) ContentType() string { return r.ct } // staticFile is a NamedReadCloser used in file-upload cases. type staticFile struct { name string r *strings.Reader ct string // optional ContentType() } func (f *staticFile) Name() string { return f.name } func (f *staticFile) Read(p []byte) (int, error) { return f.r.Read(p) } func (f *staticFile) Close() error { return nil } // staticFileWithCT implements NamedReadCloser AND ContentType() string. type staticFileWithCT struct{ *staticFile } func (f *staticFileWithCT) ContentType() string { return f.ct } func newFile(data string) runtime.NamedReadCloser { return &staticFile{name: "doc.txt", r: strings.NewReader(data)} } func newFileWithCT(name, data, ct string) runtime.NamedReadCloser { return &staticFileWithCT{staticFile: &staticFile{name: name, r: strings.NewReader(data), ct: ct}} } // --- body assertion helpers ------------------------------------------ func bodyExact(want string) func(t *testing.T, body []byte) { return func(t *testing.T, body []byte) { t.Helper() assert.EqualT(t, want, string(body)) } } func bodyContainsAll(parts ...string) func(t *testing.T, body []byte) { return func(t *testing.T, body []byte) { t.Helper() s := string(body) for _, p := range parts { assert.Contains(t, s, p, "expected body to contain %q", p) } } } func bodyEmpty() func(t *testing.T, body []byte) { return func(t *testing.T, body []byte) { t.Helper() assert.Empty(t, body) } } // --- case families --------------------------------------------------- // Family A — payload as struct, producer runs. func payloadStructCases() iter.Seq[buildHTTPCase] { return slices.Values([]buildHTTPCase{ { name: "struct + JSON producer", mediaType: runtime.JSONMime, writer: writerSetBody(task{Completed: true, Content: "ok", ID: 7}), wantContentType: runtime.JSONMime, wantBody: bodyContainsAll(`"completed":true`, `"content":"ok"`, `"id":7`), }, { name: "struct + XML producer", mediaType: runtime.XMLMime, writer: writerSetBody(task{Completed: false, Content: "x", ID: 1}), wantContentType: runtime.XMLMime, wantBody: bodyContainsAll("", "x"), }, { name: "struct + text producer", mediaType: runtime.TextMime, writer: writerSetBody("a-string-payload"), wantContentType: runtime.TextMime, wantBody: bodyExact("a-string-payload"), }, { // Deliberate non-honor: SetHeader is NOT respected for // struct payloads because the producer is dispatched off // mediaType. Honouring an arbitrary user header here would // mean either swapping the producer (complex) or sending a // body that doesn't match the declared header (still a // lie). Streams have no producer dispatch, so they get the // escape hatch; struct/form/multipart paths do not. name: "struct + SetHeader Content-Type is ignored — picker wins", mediaType: runtime.JSONMime, writer: writerCompose( writerSetContentType("application/x-ignored"), writerSetBody(task{Content: "y"}), ), wantContentType: runtime.JSONMime, wantBody: bodyContainsAll(`"content":"y"`), }, // Note: missing-producer for non-Reader payload panics today // inside buildHTTP (nil producer dereference at request.go:343). // The Submit-level gate at runtime.go:550 catches it earlier and // returns a proper error — covered by submitWiringCases. }) } // Family B — payload as io.Reader / io.ReadCloser. The producer is // bypassed; the body is the reader bytes verbatim. // // Header rules (in priority order): // 1. payload's `ContentType() string` if non-empty; // 2. application/octet-stream from the consumes list, when registered // as a producer (Stage-2 fallback); // 3. the picker's mediaType. // // Cases with empty consumes exercise the buildHTTP-direct entry point // (i.e. external callers of BuildHTTP that have already picked a mime // without going through createHTTPRequest). func payloadReaderCases() iter.Seq[buildHTTPCase] { return slices.Values([]buildHTTPCase{ { name: "io.Reader, picked octet-stream — header matches body", mediaType: runtime.DefaultMime, writer: writerSetBody(bytes.NewReader([]byte{0x01, 0x02, 0x03})), wantContentType: runtime.DefaultMime, wantBody: bodyExact("\x01\x02\x03"), }, { // No consumes context: external BuildHTTP caller already // picked JSON. Header is the picked mime even though the // body is raw bytes — we have nothing better to fall back // to. name: "io.Reader, picked JSON, no consumes context — header is picked mime", mediaType: runtime.JSONMime, writer: writerSetBody(bytes.NewReader([]byte("not json at all"))), wantContentType: runtime.JSONMime, wantBody: bodyExact("not json at all"), }, { // Stage-2 kicks in: picker chose JSON, but octet-stream is // also offered and registered, so the wire header drops the // JSON claim and advertises raw bytes instead. name: "io.Reader, picked JSON, octet-stream offered → octet-stream wins", mediaType: runtime.JSONMime, consumes: []string{runtime.JSONMime, runtime.DefaultMime}, writer: writerSetBody(bytes.NewReader([]byte("not json"))), wantContentType: runtime.DefaultMime, wantBody: bodyExact("not json"), }, { // Stage-2 has nothing to upgrade to: octet-stream is not // among the candidates, so the picker's choice is preserved. name: "io.Reader, picked text, no octet-stream offered → picked mime preserved", mediaType: runtime.TextMime, consumes: []string{runtime.TextMime, runtime.JSONMime}, writer: writerSetBody(bytes.NewReader([]byte("hi"))), wantContentType: runtime.TextMime, wantBody: bodyExact("hi"), }, { name: "io.Reader with ContentType() overrides Stage-2 fallback", mediaType: runtime.JSONMime, consumes: []string{runtime.JSONMime, runtime.DefaultMime}, writer: writerSetBody(&readerWithCT{ Reader: strings.NewReader(`{"a":1}` + "\n" + `{"a":2}`), ct: ndjsonMime, }), wantContentType: ndjsonMime, wantBody: bodyExact("{\"a\":1}\n{\"a\":2}"), }, { name: "io.Reader with empty ContentType() — Stage-2 fallback applies", mediaType: runtime.JSONMime, consumes: []string{runtime.JSONMime, runtime.DefaultMime}, writer: writerSetBody(&readerWithCT{ Reader: strings.NewReader("body"), ct: "", }), wantContentType: runtime.DefaultMime, wantBody: bodyExact("body"), }, { name: "io.ReadCloser, picked octet-stream", mediaType: runtime.DefaultMime, writer: writerSetBody(io.NopCloser(strings.NewReader("payload"))), wantContentType: runtime.DefaultMime, wantBody: bodyExact("payload"), }, { name: "io.ReadCloser, picked text, octet-stream offered → octet-stream wins", mediaType: runtime.TextMime, consumes: []string{runtime.TextMime, runtime.DefaultMime}, writer: writerSetBody(io.NopCloser(bytes.NewReader([]byte{0xFF, 0xFE}))), wantContentType: runtime.DefaultMime, wantBody: bodyExact("\xFF\xFE"), }, { name: "io.ReadCloser with ContentType() overrides Stage-2 fallback", mediaType: runtime.TextMime, consumes: []string{runtime.TextMime, runtime.DefaultMime}, writer: writerSetBody(&readCloserWithCT{ ReadCloser: io.NopCloser(strings.NewReader("ndjson")), ct: ndjsonMime, }), wantContentType: ndjsonMime, wantBody: bodyExact("ndjson"), }, { // Edge case: octet-stream offered but no producer registered // for it (caller stripped the default). Stage-2 cannot // upgrade, picker's choice preserved. name: "io.Reader, octet-stream offered but producer missing → picked mime preserved", mediaType: runtime.JSONMime, consumes: []string{runtime.JSONMime, runtime.DefaultMime}, producers: map[string]runtime.Producer{ runtime.JSONMime: runtime.JSONProducer(), }, writer: writerSetBody(bytes.NewReader([]byte("x"))), wantContentType: runtime.JSONMime, wantBody: bodyExact("x"), }, { // Highest-priority escape hatch: SetHeaderParam wins over // every derivation. Picker chose JSON, payload is a plain // Reader, but the user asserted application/x-vendor. name: "io.Reader + SetHeader Content-Type wins over picker", mediaType: runtime.JSONMime, writer: writerCompose( writerSetContentType(vendorMime), writerSetBody(bytes.NewReader([]byte("v"))), ), wantContentType: vendorMime, wantBody: bodyExact("v"), }, { // SetHeader beats payload's ContentType() declaration. name: "io.Reader + SetHeader wins over payload ContentType()", mediaType: runtime.JSONMime, writer: writerCompose( writerSetContentType("application/x-explicit"), writerSetBody(&readerWithCT{ Reader: strings.NewReader("body"), ct: ndjsonMime, }), ), wantContentType: "application/x-explicit", wantBody: bodyExact("body"), }, { // SetHeader beats Stage-2 octet-stream upgrade too. name: "io.Reader + SetHeader wins over Stage-2 octet-stream", mediaType: runtime.JSONMime, consumes: []string{runtime.JSONMime, runtime.DefaultMime}, writer: writerCompose( writerSetContentType("application/x-explicit"), writerSetBody(bytes.NewReader([]byte("v"))), ), wantContentType: "application/x-explicit", wantBody: bodyExact("v"), }, { // io.ReadCloser parity. name: "io.ReadCloser + SetHeader Content-Type wins", mediaType: runtime.TextMime, writer: writerCompose( writerSetContentType(vendorMime), writerSetBody(io.NopCloser(strings.NewReader("data"))), ), wantContentType: vendorMime, wantBody: bodyExact("data"), }, }) } // Family C — []byte payload (producer runs, encoding the slice). // // Note: []byte does not satisfy io.Reader, so it falls through to the // producer. The JSON producer base64-encodes []byte per // encoding/json's documented behaviour. func payloadByteSliceCases() iter.Seq[buildHTTPCase] { return slices.Values([]buildHTTPCase{ { name: "[]byte + JSON producer (base64-encoded as string)", mediaType: runtime.JSONMime, writer: writerSetBody([]byte("hello")), wantContentType: runtime.JSONMime, // "hello" base64 = aGVsbG8= wantBody: bodyContainsAll("aGVsbG8="), }, { name: "[]byte + ByteStreamProducer (raw bytes)", mediaType: runtime.DefaultMime, writer: writerSetBody([]byte("hello")), wantContentType: runtime.DefaultMime, wantBody: bodyExact("hello"), }, }) } // Family D — form fields only. The buildHTTP form path runs whenever // formFields > 0 OR fileFields > 0. mediaType drives the multipart // vs urlencoded choice via isMultipart. func formFieldCases() iter.Seq[buildHTTPCase] { return slices.Values([]buildHTTPCase{ { name: "form fields + urlencoded mime", mediaType: runtime.URLencodedFormMime, writer: writerCompose(writerSetForm("name", "fido"), writerSetForm("color", "brown")), wantContentType: runtime.URLencodedFormMime, wantBody: bodyContainsAll("name=fido", "color=brown"), }, { name: "form fields + multipart mime", mediaType: runtime.MultipartFormMime, writer: writerSetForm("name", "fido"), wantContentTypePrefix: runtime.MultipartFormMime + "; boundary=", wantBody: bodyContainsAll(`name="name"`, "fido"), }, { name: "form fields + non-form mime — header lies", mediaType: runtime.JSONMime, writer: writerSetForm("name", "fido"), wantContentType: runtime.JSONMime, wantBody: bodyContainsAll("name=fido"), }, }) } // Family E — file fields only. func fileFieldCases() iter.Seq[buildHTTPCase] { return slices.Values([]buildHTTPCase{ { name: "file field + multipart mime", mediaType: runtime.MultipartFormMime, writer: writerSetFileToUpload(newFile("filebody")), wantContentTypePrefix: runtime.MultipartFormMime + "; boundary=", wantBody: bodyContainsAll(`name="upload"`, `filename="doc.txt"`, "filebody"), }, { // Per the post-#286 fix, urlencoded with files is allowed and // the file content travels as a regular form value. name: "file field + urlencoded mime — file inlined as form value", mediaType: runtime.URLencodedFormMime, writer: writerSetFileToUpload(newFile("abc")), wantContentType: runtime.URLencodedFormMime, wantBody: bodyContainsAll("upload=abc"), }, { name: "file with declared ContentType()", mediaType: runtime.MultipartFormMime, writer: writerSetFileToUpload(newFileWithCT("doc.txt", "x", "application/json")), wantContentTypePrefix: runtime.MultipartFormMime + "; boundary=", wantBody: bodyContainsAll("application/json"), }, }) } // Family F — form + file fields. Multipart preferred (post-#286). func formAndFileFieldCases() iter.Seq[buildHTTPCase] { return slices.Values([]buildHTTPCase{ { name: "form + file + multipart mime", mediaType: runtime.MultipartFormMime, writer: writerCompose( writerSetForm("name", "fido"), writerSetFileToUpload(newFile("filebody")), ), wantContentTypePrefix: runtime.MultipartFormMime + "; boundary=", wantBody: bodyContainsAll(`name="name"`, "fido", `filename="doc.txt"`, "filebody"), }, { name: "form + file + urlencoded mime — file inlined", mediaType: runtime.URLencodedFormMime, writer: writerCompose( writerSetForm("name", "fido"), writerSetFileToUpload(newFile("abc")), ), wantContentType: runtime.URLencodedFormMime, wantBody: bodyContainsAll("name=fido", "upload=abc"), }, }) } // Family G — no body. func noBodyCases() iter.Seq[buildHTTPCase] { return slices.Values([]buildHTTPCase{ { name: "GET, no payload, no fields — no Content-Type", method: http.MethodGet, mediaType: runtime.JSONMime, // mediaType is ignored when there is no body writer: noopWriter(), wantContentType: "", wantBody: bodyEmpty(), }, { name: "POST, no payload, no fields — no Content-Type", method: http.MethodPost, mediaType: runtime.JSONMime, writer: noopWriter(), wantContentType: "", wantBody: bodyEmpty(), }, }) } // Family — error paths in buildHTTP / write phase. func missingProducerCases() iter.Seq[buildHTTPCase] { // buildHTTP looks up the producer at line 342 only for non-Reader // payloads. A nil producer panics there today, so we cannot easily // assert via this harness without recovering. The Submit-level // equivalent is gated earlier (runtime.go:550) and returns a // proper error — covered in submitWiringCases. return slices.Values([]buildHTTPCase{ { name: "writer returns an error", mediaType: runtime.JSONMime, writer: runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return errors.New("boom") }), wantErr: "boom", }, }) } // Family H — Submit-level: verifies picker → buildHTTP wiring through // a captured RoundTripper. func submitWiringCases() iter.Seq[submitCase] { return slices.Values([]submitCase{ { name: "consumes [json] + struct payload", consumes: []string{runtime.JSONMime}, writer: writerSetBody(task{Content: "x"}), wantContentType: runtime.JSONMime, wantBody: bodyContainsAll(`"content":"x"`), }, { name: "consumes [multipart, urlencoded] + form fields → multipart wins", consumes: []string{runtime.URLencodedFormMime, runtime.MultipartFormMime}, writer: writerSetForm("name", "fido"), wantContentTypePrefix: runtime.MultipartFormMime + "; boundary=", wantBody: bodyContainsAll(`name="name"`, "fido"), }, { // Stage-2 fix: picker chose JSON (first non-empty), but // the payload is a stream and octet-stream is offered — // so the wire header is upgraded to octet-stream. name: "consumes [json, octet] + io.Reader → octet-stream wins (Stage-2)", consumes: []string{runtime.JSONMime, runtime.DefaultMime}, writer: writerSetBody(bytes.NewReader([]byte("not json"))), wantContentType: runtime.DefaultMime, wantBody: bodyExact("not json"), }, { // Picker chose JSON; no octet-stream offered. Stage-2 has // nothing to upgrade to, so the wire header is JSON even // though the body is raw bytes. name: "consumes [json] + io.Reader without alternative → header is picked JSON", consumes: []string{runtime.JSONMime}, writer: writerSetBody(bytes.NewReader([]byte("raw"))), wantContentType: runtime.JSONMime, wantBody: bodyExact("raw"), }, { // SetHeader escape hatch surfaces through Submit too. name: "consumes [json] + SetHeader Content-Type — escape hatch wins", consumes: []string{runtime.JSONMime}, writer: writerCompose( writerSetContentType(vendorMime), writerSetBody(bytes.NewReader([]byte("data"))), ), wantContentType: vendorMime, wantBody: bodyExact("data"), }, { name: "consumes [json] + io.Reader with ContentType() — declared type wins", consumes: []string{runtime.JSONMime}, writer: writerSetBody(&readerWithCT{ Reader: strings.NewReader(`{"line":1}` + "\n" + `{"line":2}`), ct: ndjsonMime, }), wantContentType: ndjsonMime, wantBody: bodyExact("{\"line\":1}\n{\"line\":2}"), }, { name: "consumes lists only an unregistered producer — error before send", consumes: []string{"application/vnd.example"}, writer: writerSetBody(task{Content: "x"}), wantErr: "none of producers", }, { // Producer-capability filter: spec lists a vendor mime first // but no vendor producer is registered. Picker now falls // through to the registered JSON entry instead of erroring. // Resolves issues #32, #386 (filter rule). name: "consumes [vendor, json] with no vendor producer → JSON wins", consumes: []string{"application/x-vendor", runtime.JSONMime}, writer: writerSetBody(task{Content: "x"}), wantContentType: runtime.JSONMime, wantBody: bodyContainsAll(`"content":"x"`), }, { // All entries unregistered: picker preserves the first // non-empty so the runtime.go gate fires with the historical // diagnostic. name: "consumes lists only unregistered producers → error before send", consumes: []string{vendorMime1, vendorMime2}, writer: writerSetBody(task{Content: "x"}), wantErr: "none of producers", }, { name: "empty consumes falls back to DefaultMediaType (json)", consumes: nil, writer: writerSetBody(task{Content: "x"}), wantContentType: runtime.JSONMime, wantBody: bodyContainsAll(`"content":"x"`), }, }) } go-openapi-runtime-decad8f/client/gencerts_test.go000066400000000000000000000217241520232310000225410ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2026 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "math/big" "net" "os" "path/filepath" "strings" "testing" "time" "github.com/go-openapi/testify/v2/require" ) const ( // X509 material: keys and certificates to test TLS myClientKey = "myclient.key" myClientCert = "myclient.crt" myCAKey = "myCA.key" myCACert = "myCA.crt" myServerKey = "mycert1.key" myServerCert = "mycert1.crt" myClientECCKey = "myclient-ecc.key" myClientECCCert = "myclient-ecc.crt" ) // newTLSFixtures loads TLS material for testing. func newTLSFixtures(t testing.TB) *tlsFixtures { const subject = "somewhere" certFixturesDir := t.TempDir() require.NoError(t, runGenCerts(t, certFixturesDir)) keyFile := filepath.Join(certFixturesDir, myClientKey) keyPem, err := os.ReadFile(keyFile) require.NoError(t, err) keyDer, _ := pem.Decode(keyPem) require.NotNil(t, keyDer) key, err := x509.ParsePKCS1PrivateKey(keyDer.Bytes) require.NoError(t, err) certFile := filepath.Join(certFixturesDir, myClientCert) certPem, err := os.ReadFile(certFile) require.NoError(t, err) certDer, _ := pem.Decode(certPem) require.NotNil(t, certDer) cert, err := x509.ParseCertificate(certDer.Bytes) require.NoError(t, err) eccKeyFile := filepath.Join(certFixturesDir, myClientECCKey) eckeyPem, err := os.ReadFile(eccKeyFile) require.NoError(t, err) eccBlock, remainder := pem.Decode(eckeyPem) ecKeyDer, _ := pem.Decode(remainder) require.Nil(t, ecKeyDer) ecKey, err := x509.ParseECPrivateKey(eccBlock.Bytes) require.NoError(t, err) eccCertFile := filepath.Join(certFixturesDir, myClientECCCert) ecCertPem, err := os.ReadFile(eccCertFile) require.NoError(t, err) ecCertDer, _ := pem.Decode(ecCertPem) require.NotNil(t, ecCertDer) ecCert, err := x509.ParseCertificate(ecCertDer.Bytes) require.NoError(t, err) caFile := filepath.Join(certFixturesDir, myCACert) caPem, err := os.ReadFile(caFile) require.NoError(t, err) caBlock, _ := pem.Decode(caPem) require.NotNil(t, caBlock) caCert, err := x509.ParseCertificate(caBlock.Bytes) require.NoError(t, err) serverKeyFile := filepath.Join(certFixturesDir, myServerKey) serverKeyPem, err := os.ReadFile(serverKeyFile) require.NoError(t, err) serverKeyDer, _ := pem.Decode(serverKeyPem) require.NotNil(t, serverKeyDer) serverKey, err := x509.ParseECPrivateKey(serverKeyDer.Bytes) require.NoError(t, err) serverCertFile := filepath.Join(certFixturesDir, myServerCert) serverCertPem, err := os.ReadFile(serverCertFile) require.NoError(t, err) serverCertDer, _ := pem.Decode(serverCertPem) require.NotNil(t, serverCertDer) serverCert, err := x509.ParseCertificate(serverCertDer.Bytes) require.NoError(t, err) return &tlsFixtures{ Subject: subject, RSA: tlsFixture{ CAFile: caFile, KeyFile: keyFile, CertFile: certFile, LoadedCA: caCert, LoadedKey: key, LoadedCert: cert, }, ECDSA: tlsFixture{ KeyFile: eccKeyFile, CertFile: eccCertFile, LoadedKey: ecKey, LoadedCert: ecCert, }, Server: tlsFixture{ KeyFile: serverKeyFile, CertFile: serverCertFile, LoadedCA: caCert, LoadedKey: serverKey, LoadedCert: serverCert, }, } } // runGenCerts generates self-signed TLS certificates for the todo-list-errors example. // // It produces: // - myCA.key / myCA.crt — self-signed certificate authority // - mycert1.key / mycert1.crt — server certificate (CN=goswagger.local) // - myclient.key / myclient.crt — RSA client certificate (CN=localhost) // - myclient-ecc.key / myclient-ecc.crt — ECDSA client certificate (CN=localhost) // // All ECDSA certificates use ECDSA P-256. All certificates are valid for 10 years. func runGenCerts(t testing.TB, outDir string) error { t.Logf("Generating TLS certificates in %s", outDir) // Generate CA caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return fmt.Errorf("generating CA key: %w", err) } caTemplate := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "Go Swagger"}, NotBefore: time.Now(), NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, IsCA: true, } caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) if err != nil { return fmt.Errorf("creating CA certificate: %w", err) } caCert, err := x509.ParseCertificate(caCertDER) if err != nil { return fmt.Errorf("parsing CA certificate: %w", err) } if err := writeKeyPair(outDir, stem(myCACert), caKey, caCertDER); err != nil { return err } // Generate server certificate serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return fmt.Errorf("generating server key: %w", err) } serverTemplate := &x509.Certificate{ SerialNumber: big.NewInt(2), Subject: pkix.Name{CommonName: "goswagger.local"}, NotBefore: time.Now(), NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: []string{"goswagger.local", "localhost", "www.example.com"}, IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, } serverCertDER, err := x509.CreateCertificate(rand.Reader, serverTemplate, caCert, &serverKey.PublicKey, caKey) if err != nil { return fmt.Errorf("creating server certificate: %w", err) } if err := writeKeyPair(outDir, stem(myServerKey), serverKey, serverCertDER); err != nil { return err } // Generate client certificate // RSA client cert clientRSAKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return fmt.Errorf("generating client RSA key: %w", err) } clientTemplate := makeCertReqTemplate(3) clientRSACertDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, caCert, &clientRSAKey.PublicKey, caKey) if err != nil { return fmt.Errorf("creating client RSA certificate: %w", err) } if err := writePKCS1KeyPair(outDir, stem(myClientCert), clientRSAKey, clientRSACertDER); err != nil { return err } // ECC client cert clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return fmt.Errorf("generating client key: %w", err) } clientTemplate = makeCertReqTemplate(4) clientCertDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, caCert, &clientKey.PublicKey, caKey) if err != nil { return fmt.Errorf("creating client ECDSA certificate: %w", err) } if err := writeKeyPair(outDir, stem(myClientECCCert), clientKey, clientCertDER); err != nil { return err } t.Logf(" %s / %s — certificate authority", myCAKey, myCACert) t.Logf(" %s / %s — server (CN=goswagger.local)", myServerKey, myServerCert) t.Logf(" %s / %s — client (RSA, CN=localhost)", myClientKey, myClientCert) t.Logf(" %s / %s — client (ECDSA, CN=localhost)", myClientECCKey, myClientECCCert) return nil } func makeCertReqTemplate(n int64) *x509.Certificate { return &x509.Certificate{ SerialNumber: big.NewInt(n), Subject: pkix.Name{ CommonName: "localhost", Country: []string{"US"}, Province: []string{"California"}, Locality: []string{"San Francisco"}, Organization: []string{"go-swagger"}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } } func writeKeyPair(dir, name string, key *ecdsa.PrivateKey, certDER []byte) error { keyPath := filepath.Join(dir, name+".key") certPath := filepath.Join(dir, name+".crt") // Write private key keyDER, err := x509.MarshalECPrivateKey(key) if err != nil { return fmt.Errorf("marshaling %s key: %w", name, err) } if err := writePEM(keyPath, "EC PRIVATE KEY", keyDER); err != nil { return err } // Write certificate if err := writePEM(certPath, "CERTIFICATE", certDER); err != nil { return err } return nil } func writePKCS1KeyPair(dir, name string, key *rsa.PrivateKey, certDER []byte) error { keyPath := filepath.Join(dir, name+".key") certPath := filepath.Join(dir, name+".crt") // Write private key keyDER := x509.MarshalPKCS1PrivateKey(key) if err := writePEM(keyPath, "RSA PRIVATE KEY", keyDER); err != nil { return err } // Write certificate if err := writePEM(certPath, "CERTIFICATE", certDER); err != nil { return err } return nil } func writePEM(path, blockType string, data []byte) error { f, err := os.Create(path) if err != nil { return fmt.Errorf("creating %s: %w", path, err) } defer f.Close() return pem.Encode(f, &pem.Block{Type: blockType, Bytes: data}) } func stem(file string) string { s := strings.Split(file, ".") return s[0] } go-openapi-runtime-decad8f/client/httptrace.go000066400000000000000000000366251520232310000216740ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "context" "crypto/tls" "fmt" "io" "net/http/httptrace" "strings" "sync" "time" "github.com/go-openapi/runtime/logger" ) // traceSession owns the per-request state for [Runtime.Trace]. // // It tracks the t=0 anchor for the connection phase, accumulates // per-phase timestamps (for the trailing summary), and emits each // event to the runtime logger as it fires. One session per // SubmitContext call. type traceSession struct { logger logger.Logger method string url string // tlsCfg points at the *tls.Config of the http.Transport that // will run the request, when introspectable (i.e. the transport // is an *http.Transport). Used by the TLS diagnostic mode to // cross-check user configuration against what the handshake // actually attempted. Nil when the transport is custom and // the config cannot be reached. tlsCfg *tls.Config mu sync.Mutex start time.Time last time.Time // last printed event, for relative-dt rendering phases phaseTimings gotConn httptrace.GotConnInfo tlsDone tlsResult dnsStartAt time.Time connectStartAt time.Time tlsHandshakeStartAt time.Time wait100StartAt time.Time gotConnAt time.Time wroteHeadersAt time.Time wroteRequestAt time.Time ttfbAt time.Time statusCode int rtError error } // phaseTimings holds the per-phase durations for the trailing // summary line. Zero values mean "phase did not occur" (e.g. no // DNS lookup on a reused conn, no TLS on http://). type phaseTimings struct { dns time.Duration dial time.Duration tls time.Duration ttfb time.Duration // time from GotConn to first response byte } // tlsResult captures whatever we learned from TLSHandshakeDone. // On the happy path err is nil and state is fully populated; on // failure state may be partial (and is what the TLS diagnostic // mode in httptrace_tls.go works from). type tlsResult struct { state tls.ConnectionState err error done bool } const tracePrefix = "[trace] " // staleIdleThreshold is the idle duration above which a reused // pooled connection earns a HEADS-UP annotation. Per-runtime // configurability is deferred to v2; 30s matches the issue #336 // territory (typical NAT idle timeouts start in the 60–350s // range, so a 30s reuse is already in "could be stale" zone). const staleIdleThreshold = 30 * time.Second // newTraceSession allocates a session and pre-renders the opening // line (method + url). The session is not yet attached to a // context — that's the caller's responsibility via session.attach. // // tlsCfg may be nil; when non-nil it is used by the TLS diagnostic // mode to cross-check user-configured constraints (MinVersion, // CipherSuites, custom RootCAs) against handshake failures. func newTraceSession(log logger.Logger, method, url string, tlsCfg *tls.Config) *traceSession { s := &traceSession{ logger: log, method: method, url: url, tlsCfg: tlsCfg, start: time.Now(), } s.last = s.start s.emitf("%s %s", method, url) return s } // attach installs the session's ClientTrace on ctx and returns the // derived context. Callers pass the returned context to // http.Client.Do (typically by setting it on req via // req.WithContext) so the transport fires the hooks. func (s *traceSession) attach(ctx context.Context) context.Context { return httptrace.WithClientTrace(ctx, s.clientTrace()) } // clientTrace wires every httptrace hook to the corresponding // session method. Each callback is responsible for its own // locking; the stdlib does not serialize trace callbacks. func (s *traceSession) clientTrace() *httptrace.ClientTrace { return &httptrace.ClientTrace{ GetConn: s.onGetConn, GotConn: s.onGotConn, PutIdleConn: s.onPutIdleConn, GotFirstResponseByte: s.onGotFirstResponseByte, Got100Continue: s.onGot100Continue, DNSStart: s.onDNSStart, DNSDone: s.onDNSDone, ConnectStart: s.onConnectStart, ConnectDone: s.onConnectDone, TLSHandshakeStart: s.onTLSHandshakeStart, TLSHandshakeDone: s.onTLSHandshakeDone, WroteHeaders: s.onWroteHeaders, Wait100Continue: s.onWait100Continue, WroteRequest: s.onWroteRequest, } } // --------------------------------------------------------------- // Phase callbacks (stdlib httptrace hooks) // --------------------------------------------------------------- func (s *traceSession) onGetConn(hostPort string) { s.emitTf("GetConn(%s)", hostPort) } func (s *traceSession) onGotConn(info httptrace.GotConnInfo) { s.mu.Lock() s.gotConn = info s.gotConnAt = time.Now() s.mu.Unlock() if info.Reused { s.emitTf("GotConn(reused=true, idle=%t, idle-time=%s)", info.WasIdle, info.IdleTime.Round(time.Millisecond)) } else { s.emitTf("GotConn(reused=false)") } if isStaleIdleReuse(info) { s.emitf("# HEADS-UP: reused idle connection (idle for %s).", info.IdleTime.Round(time.Second)) s.emitf("# If this request fails with EOF/connection reset, the server") s.emitf("# or an in-path NAT may have dropped the conn silently.") } } // isStaleIdleReuse reports whether a GotConn info indicates the // connection came from the idle pool after sitting idle for // longer than [staleIdleThreshold]. This is the issue #336 // pattern: long-idle pooled conns are the ones most likely to be // dead by the time the next request tries to use them. func isStaleIdleReuse(info httptrace.GotConnInfo) bool { return info.Reused && info.WasIdle && info.IdleTime > staleIdleThreshold } func (s *traceSession) onPutIdleConn(err error) { if err != nil { s.emitTf("PutIdleConn(err=%v)", err) return } s.emitTf("PutIdleConn") } func (s *traceSession) onGotFirstResponseByte() { s.mu.Lock() s.ttfbAt = time.Now() if !s.gotConnAt.IsZero() { s.phases.ttfb = s.ttfbAt.Sub(s.gotConnAt) } s.mu.Unlock() s.emitTf("GotFirstResponseByte (TTFB)") } func (s *traceSession) onGot100Continue() { s.emitTf("Got100Continue") } func (s *traceSession) onDNSStart(info httptrace.DNSStartInfo) { s.mu.Lock() s.dnsStartAt = time.Now() s.mu.Unlock() s.emitTf("DNSStart(host=%s)", info.Host) } func (s *traceSession) onDNSDone(info httptrace.DNSDoneInfo) { s.mu.Lock() if !s.dnsStartAt.IsZero() { s.phases.dns = time.Since(s.dnsStartAt) } s.mu.Unlock() addrs := make([]string, 0, len(info.Addrs)) for _, a := range info.Addrs { addrs = append(addrs, a.String()) } if info.Err != nil { s.emitTf("DNSDone(err=%v, addrs=[%s], coalesced=%t)", info.Err, strings.Join(addrs, " "), info.Coalesced) return } s.emitTf("DNSDone(addrs=[%s], coalesced=%t)", strings.Join(addrs, " "), info.Coalesced) } func (s *traceSession) onConnectStart(network, addr string) { s.mu.Lock() s.connectStartAt = time.Now() s.mu.Unlock() s.emitTf("ConnectStart(%s %s)", network, addr) } func (s *traceSession) onConnectDone(network, addr string, err error) { s.mu.Lock() if !s.connectStartAt.IsZero() { s.phases.dial = time.Since(s.connectStartAt) } s.mu.Unlock() if err != nil { s.emitTf("ConnectDone(%s %s, err=%v)", network, addr, err) return } s.emitTf("ConnectDone(%s %s)", network, addr) } func (s *traceSession) onTLSHandshakeStart() { s.mu.Lock() s.tlsHandshakeStartAt = time.Now() s.mu.Unlock() s.emitTf("TLSHandshakeStart") } func (s *traceSession) onTLSHandshakeDone(state tls.ConnectionState, err error) { s.mu.Lock() if !s.tlsHandshakeStartAt.IsZero() { s.phases.tls = time.Since(s.tlsHandshakeStartAt) } s.tlsDone = tlsResult{state: state, err: err, done: true} s.mu.Unlock() if err != nil { s.emitTf("TLSHandshakeDone(err=%v)", err) s.emitTLSDiagnostic(state, err) return } s.emitTf("TLSHandshakeDone(tls=%s, cipher=%s, server=%s%s)", tlsVersionName(state.Version), tls.CipherSuiteName(state.CipherSuite), state.ServerName, certExpiryFragment(state), ) } func (s *traceSession) onWroteHeaders() { s.mu.Lock() s.wroteHeadersAt = time.Now() s.mu.Unlock() s.emitTf("WroteHeaders") } func (s *traceSession) onWait100Continue() { s.mu.Lock() s.wait100StartAt = time.Now() s.mu.Unlock() s.emitTf("Wait100Continue") } func (s *traceSession) onWroteRequest(info httptrace.WroteRequestInfo) { s.mu.Lock() s.wroteRequestAt = time.Now() s.mu.Unlock() if info.Err != nil { s.emitTf("WroteRequest(err=%v)", info.Err) return } s.emitTf("WroteRequest") } // --------------------------------------------------------------- // Body wrapping // --------------------------------------------------------------- // bodySide identifies which direction an instrumented body is on. type bodySide string const ( bodySend bodySide = "Sent" bodyRecv bodySide = "Received" ) // instrumentedBody wraps an [io.ReadCloser] and emits a // BodyChunk{Sent,Received} trace event per Read call. Tracks the // inter-read delay in `dt` so users can see streaming-body // cadence. // // Read granularity: bytes returned by the underlying body, not // HTTP/1.1 chunked-framing units. For wire-level chunking, use // [Runtime.Debug] instead. // // Concurrency: a single body is read from a single goroutine in // practice (http.Transport for request bodies, the application // for response bodies), so no internal locking is needed beyond // what the underlying ReadCloser provides. type instrumentedBody struct { wrapped io.ReadCloser sess *traceSession side bodySide last time.Time } func (b *instrumentedBody) Read(p []byte) (int, error) { n, err := b.wrapped.Read(p) if n > 0 { first := b.last.IsZero() var dt time.Duration if !first { dt = time.Since(b.last) } b.last = time.Now() b.sess.onBodyChunk(b.side, n, dt, first) } return n, err } func (b *instrumentedBody) Close() error { return b.wrapped.Close() } // wrapRequestBody returns an instrumented wrapper around the // outgoing request body, or the original body if nil (which is // the common case for GET requests). The wrapper observes // Transport-side reads, so BodyChunkSent events appear between // WroteHeaders and WroteRequest in the trace timeline. func (s *traceSession) wrapRequestBody(body io.ReadCloser) io.ReadCloser { if body == nil { return nil } return &instrumentedBody{wrapped: body, sess: s, side: bodySend} } // wrapResponseBody returns an instrumented wrapper around the // incoming response body. Stacks cleanly above // [KeepAliveTransport]'s drain-on-close behavior. func (s *traceSession) wrapResponseBody(body io.ReadCloser) io.ReadCloser { if body == nil { return nil } return &instrumentedBody{wrapped: body, sess: s, side: bodyRecv} } // onBodyChunk renders a single BodyChunk{Sent,Received} event. // dt is the duration since the previous Read on the same body and // is meaningful only when `first` is false. The first chunk has no // preceding read, so the dt= field is suppressed; every subsequent // chunk emits dt= unconditionally — even when the measured value // rounds to zero (common on Windows, where the system clock // resolution is coarser than a fast loopback read loop). func (s *traceSession) onBodyChunk(side bodySide, n int, dt time.Duration, first bool) { if first { s.emitTf("BodyChunk%s(n=%d)", side, n) return } s.emitTf("BodyChunk%s(n=%d, dt=%s)", side, n, round(dt)) } // --------------------------------------------------------------- // Submit-level lifecycle hooks (called from SubmitContext) // --------------------------------------------------------------- // onRoundTripError is called by SubmitContext when http.Client.Do // returns an error. It records the error for the summary line. func (s *traceSession) onRoundTripError(err error) { s.mu.Lock() s.rtError = err s.mu.Unlock() s.emitTf("! error: %v", err) } // onResponse is called when http.Client.Do returns successfully. // It records the status code for the summary line. func (s *traceSession) onResponse(statusCode int) { s.mu.Lock() s.statusCode = statusCode s.mu.Unlock() } // finish renders the trailing single-line summary and is called // by SubmitContext after the response body has been consumed (or // on error path, after the error was recorded). When a round-trip // error happened on a stale-idle reused connection, a tail block // flags the issue #336 pattern explicitly. func (s *traceSession) finish() { s.mu.Lock() defer s.mu.Unlock() total := time.Since(s.start) var b strings.Builder fmt.Fprintf(&b, "Summary: %s — ", s.method) if s.rtError != nil { fmt.Fprintf(&b, "FAILED (%v)", s.rtError) } else { fmt.Fprintf(&b, "%d", s.statusCode) } if s.phases.dns > 0 { fmt.Fprintf(&b, ", dns=%s", round(s.phases.dns)) } if s.phases.dial > 0 { fmt.Fprintf(&b, ", dial=%s", round(s.phases.dial)) } if s.phases.tls > 0 { fmt.Fprintf(&b, ", tls=%s", round(s.phases.tls)) } if s.phases.ttfb > 0 { fmt.Fprintf(&b, ", ttfb=%s", round(s.phases.ttfb)) } fmt.Fprintf(&b, ", total=%s", round(total)) s.emitRaw(b.String()) // issue #336 tail annotation: a round-trip failure on a // stale-idle reused conn is the canonical pattern. if s.rtError != nil && isStaleIdleReuse(s.gotConn) { s.emitf("# FAILED on a reused idle conn (%s idle).", s.gotConn.IdleTime.Round(time.Second)) s.emitf("# Silently closed the conn while it sat in the idle pool.") s.emitf("# Consider lowering http.Transport.IdleConnTimeout to evict") s.emitf("# pooled conns before the NAT/server side does.") } } // --------------------------------------------------------------- // Emission helpers // --------------------------------------------------------------- // emitf prints a plain event line (no t= timestamp). Used for the // opening line and the summary. func (s *traceSession) emitf(format string, args ...any) { s.logger.Debugf(tracePrefix+format, args...) } // emitRaw is like emitf but takes an already-rendered string. Used // by finish() which builds its line via strings.Builder. func (s *traceSession) emitRaw(line string) { s.logger.Debugf("%s", tracePrefix+line) } // emitTf prints a phase event with a cumulative t=... offset from // the session start. func (s *traceSession) emitTf(format string, args ...any) { t := round(time.Since(s.start)) msg := fmt.Sprintf(format, args...) s.logger.Debugf(tracePrefix+"%s (t=%s)", msg, t) } // traceRoundUnit is the rounding granularity for >=1ms durations // rendered in trace output. 100µs keeps lines readable while // preserving enough resolution to spot millisecond-scale phase // differences. const traceRoundUnit = 100 * time.Microsecond // round trims durations for human-readable trace output. // Sub-millisecond durations round to 1µs (preserves visibility on // fast loopback servers); >=1ms durations round to [traceRoundUnit]. func round(d time.Duration) time.Duration { if d <= 0 { return 0 } if d < time.Millisecond { return d.Round(time.Microsecond) } return d.Round(traceRoundUnit) } // --------------------------------------------------------------- // TLS rendering helpers // --------------------------------------------------------------- func tlsVersionName(v uint16) string { switch v { case tls.VersionTLS10: return "1.0" case tls.VersionTLS11: return "1.1" case tls.VersionTLS12: return "1.2" case tls.VersionTLS13: return "1.3" default: return fmt.Sprintf("0x%04x", v) } } // certExpiryFragment renders ", expires=YYYY-MM-DD" for the leaf // cert when available, or an empty string otherwise. func certExpiryFragment(state tls.ConnectionState) string { if len(state.PeerCertificates) == 0 { return "" } return ", expires=" + state.PeerCertificates[0].NotAfter.UTC().Format("2006-01-02") } go-openapi-runtime-decad8f/client/httptrace_test.go000066400000000000000000000622061520232310000227250ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "bytes" "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "io" "math/big" "net" "net/http" "net/http/httptest" "net/http/httptrace" "net/url" "strings" "sync" "testing" "time" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) // testOpGetOk is a placeholder operation ID used by tests that // don't care about the value beyond it being non-empty. const testOpGetOk = "getOk" // recordingLogger captures Debugf output for trace assertions. // Printf is a no-op — Trace only ever calls Debugf. type recordingLogger struct { mu sync.Mutex lines []string } func (l *recordingLogger) Printf(string, ...any) {} func (l *recordingLogger) Debugf(format string, args ...any) { l.mu.Lock() defer l.mu.Unlock() l.lines = append(l.lines, fmt.Sprintf(format, args...)) } func (l *recordingLogger) snapshot() []string { l.mu.Lock() defer l.mu.Unlock() out := make([]string, len(l.lines)) copy(out, l.lines) return out } // containsLineWith reports whether any captured line contains // every needle (substring conjunction). Useful when we care about // ordering loosely or about presence rather than exact wording. func containsLineWith(lines []string, needles ...string) bool { for _, line := range lines { ok := true for _, n := range needles { if !strings.Contains(line, n) { ok = false break } } if ok { return true } } return false } // orderedSubsequence asserts that the given prefixes appear in // `lines` in the given order (not necessarily contiguous). func orderedSubsequence(t *testing.T, lines []string, prefixes ...string) { t.Helper() i := 0 for _, line := range lines { if i >= len(prefixes) { return } if strings.Contains(line, prefixes[i]) { i++ } } if i < len(prefixes) { t.Fatalf("expected ordered subsequence %v, only matched %d. lines:\n%s", prefixes, i, strings.Join(lines, "\n")) } } func TestRuntime_Trace_HappyPath(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Set(runtime.HeaderContentType, runtime.JSONMime) rw.WriteHeader(http.StatusOK) _, _ = rw.Write([]byte(`{"ok":true}`)) })) defer server.Close() hu, err := url.Parse(server.URL) require.NoError(t, err) rec := &recordingLogger{} rt := New(hu.Host, "/", []string{schemeHTTP}) rt.Trace = true rt.SetLogger(rec) rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) _, err = rt.Submit(&runtime.ClientOperation{ ID: testOpGetOk, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, _ runtime.Consumer) (any, error) { if response.Code() != http.StatusOK { return nil, errors.New("unexpected status") } return struct{}{}, nil }), }) require.NoError(t, err) lines := rec.snapshot() require.NotEmpty(t, lines, "expected trace output, got none") // Opening line includes method + URL. assert.True(t, containsLineWith(lines, "[trace]", "GET", server.URL+"/"), "opening line missing method+url; got:\n%s", strings.Join(lines, "\n")) // Phase sequence: GetConn → DNSStart (httptest uses 127.0.0.1 // so DNS may be skipped; don't require it) → ConnectStart → // ConnectDone → GotConn → WroteHeaders → WroteRequest → // GotFirstResponseByte → PutIdleConn → Summary. orderedSubsequence(t, lines, "GetConn(", "ConnectStart(", "ConnectDone(", "GotConn(", "WroteHeaders", "WroteRequest", "GotFirstResponseByte", "Summary:", ) // Summary line ends with a total= field and reflects status 200. var summary string for _, line := range lines { if strings.Contains(line, "Summary:") { summary = line } } assert.Contains(t, summary, "200") assert.Contains(t, summary, "total=") } func TestRuntime_Trace_DisabledByDefault(t *testing.T) { // Confirms r.Trace defaults to false even when SWAGGER_DEBUG / // DEBUG would have set r.Debug = true. This is the env-var // decoupling contract. t.Setenv("SWAGGER_DEBUG", "1") rt := New("example.com", "/", []string{schemeHTTPS}) assert.False(t, rt.Trace, "Trace must default to false regardless of SWAGGER_DEBUG") // r.Debug remains coupled for now (v2 removal); confirm it's // the only one affected. assert.True(t, rt.Debug, "Debug seed from SWAGGER_DEBUG still in effect (v1 behaviour)") } // TestRuntime_Trace_BodyChunkReceived exercises the response-side // body wrapper: a server returns a payload large enough to force // multiple Read calls by the consumer side, and we assert that // each Read shows up as a BodyChunkReceived event. func TestRuntime_Trace_BodyChunkReceived(t *testing.T) { // 64 KiB payload, read 4 KiB at a time → at least a few // BodyChunkReceived events. const ( payloadSize = 64 * 1024 readSize = 4 * 1024 ) payload := bytes.Repeat([]byte("x"), payloadSize) server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Set(runtime.HeaderContentType, runtime.DefaultMime) rw.WriteHeader(http.StatusOK) _, _ = rw.Write(payload) })) defer server.Close() hu, err := url.Parse(server.URL) require.NoError(t, err) rec := &recordingLogger{} rt := New(hu.Host, "/", []string{schemeHTTP}) rt.Trace = true rt.SetLogger(rec) rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) _, err = rt.Submit(&runtime.ClientOperation{ ID: "getBlob", Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, _ runtime.Consumer) (any, error) { // Drain the body in fixed-size chunks so each Read on // the wrapped body produces a BodyChunkReceived event. buf := make([]byte, readSize) var total int for { n, rerr := response.Body().Read(buf) total += n if rerr == io.EOF { break } if rerr != nil { return nil, rerr } } require.Equal(t, payloadSize, total) return struct{}{}, nil }), }) require.NoError(t, err) lines := rec.snapshot() // At least one BodyChunkReceived event should fire. Exact // count depends on the Transport's internal buffering. count := 0 for _, line := range lines { if strings.Contains(line, "BodyChunkReceived(") { count++ } } assert.Positive(t, count, "expected at least one BodyChunkReceived; lines:\n%s", strings.Join(lines, "\n")) // Subsequent events on the same body should carry a dt= field. if count > 1 { assert.True(t, containsLineWith(lines, "BodyChunkReceived(", "dt="), "expected dt= on a subsequent BodyChunkReceived; lines:\n%s", strings.Join(lines, "\n")) } } // TestRuntime_Trace_BodyChunkSent exercises the request-side body // wrapper: a POST with a streaming body should produce at least // one BodyChunkSent event before WroteRequest. func TestRuntime_Trace_BodyChunkSent(t *testing.T) { // Use a body big enough that Transport actually reads from it. const payloadSize = 8 * 1024 payload := bytes.Repeat([]byte("y"), payloadSize) server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { _, _ = io.Copy(io.Discard, req.Body) rw.WriteHeader(http.StatusNoContent) })) defer server.Close() hu, err := url.Parse(server.URL) require.NoError(t, err) rec := &recordingLogger{} rt := New(hu.Host, "/", []string{schemeHTTP}) rt.Trace = true rt.SetLogger(rec) rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetBodyParam(bytes.NewReader(payload)) }) _, err = rt.Submit(&runtime.ClientOperation{ ID: "postBlob", Method: http.MethodPost, PathPattern: "/", Params: rwrtr, ProducesMediaTypes: []string{runtime.DefaultMime}, ConsumesMediaTypes: []string{runtime.DefaultMime}, Reader: runtime.ClientResponseReaderFunc(func(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return struct{}{}, nil }), }) require.NoError(t, err) lines := rec.snapshot() sent := 0 for _, line := range lines { if strings.Contains(line, "BodyChunkSent(") { sent++ } } assert.Positive(t, sent, "expected at least one BodyChunkSent on a POST with a non-empty body; lines:\n%s", strings.Join(lines, "\n")) // BodyChunkSent events must precede WroteRequest in the timeline. orderedSubsequence(t, lines, "BodyChunkSent(", "WroteRequest") } // TestRuntime_Trace_StaleIdleAnnotation forges a GotConn event // reporting a long-idle reuse so the HEADS-UP annotation fires // without depending on real time-passes. We invoke the trace // session directly because reproducing a 30s+ idle conn through // the real Transport in a unit test would be both slow and flaky. func TestRuntime_Trace_StaleIdleAnnotation(t *testing.T) { rec := &recordingLogger{} sess := newTraceSession(rec, http.MethodGet, "http://example.com/api", nil) sess.onGotConn(httptrace.GotConnInfo{ Reused: true, WasIdle: true, IdleTime: 47 * time.Second, }) lines := rec.snapshot() assert.True(t, containsLineWith(lines, "GotConn(reused=true", "idle=true", "idle-time=47s"), "GotConn line missing or malformed; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "HEADS-UP", "reused idle connection"), "HEADS-UP annotation missing; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "NAT may have dropped"), "HEADS-UP body missing NAT pointer; got:\n%s", strings.Join(lines, "\n")) } // TestRuntime_Trace_StaleIdleFailureSummary verifies that a // round-trip error on a stale-idle reused conn triggers the // issue-#336 tail block in the summary. func TestRuntime_Trace_StaleIdleFailureSummary(t *testing.T) { rec := &recordingLogger{} sess := newTraceSession(rec, http.MethodGet, "http://example.com/api", nil) sess.onGotConn(httptrace.GotConnInfo{ Reused: true, WasIdle: true, IdleTime: 90 * time.Second, }) sess.onRoundTripError(io.EOF) sess.finish() lines := rec.snapshot() assert.True(t, containsLineWith(lines, "Summary:", "FAILED", "EOF"), "summary line missing FAILED/EOF; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "Silently closed"), "issue-#336 tail annotation missing; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "IdleConnTimeout"), "tail annotation missing IdleConnTimeout pointer; got:\n%s", strings.Join(lines, "\n")) } // TestRuntime_Trace_FreshConnNoAnnotation guards against false // positives: a freshly-dialed (Reused=false) conn must never // trigger the HEADS-UP / issue-#336 blocks. func TestRuntime_Trace_FreshConnNoAnnotation(t *testing.T) { rec := &recordingLogger{} sess := newTraceSession(rec, http.MethodGet, "http://example.com/api", nil) sess.onGotConn(httptrace.GotConnInfo{Reused: false}) sess.onRoundTripError(io.EOF) sess.finish() lines := rec.snapshot() assert.False(t, containsLineWith(lines, "HEADS-UP"), "HEADS-UP should NOT fire on a fresh conn; got:\n%s", strings.Join(lines, "\n")) assert.False(t, containsLineWith(lines, "issue-#336"), "issue-#336 tail should NOT fire on a fresh conn; got:\n%s", strings.Join(lines, "\n")) } // TestRuntime_Trace_ShortIdleNoAnnotation guards the threshold: // an idle-time below [staleIdleThreshold] must not trigger the // HEADS-UP block. func TestRuntime_Trace_ShortIdleNoAnnotation(t *testing.T) { rec := &recordingLogger{} sess := newTraceSession(rec, http.MethodGet, "http://example.com/api", nil) sess.onGotConn(httptrace.GotConnInfo{ Reused: true, WasIdle: true, IdleTime: 5 * time.Second, }) lines := rec.snapshot() assert.False(t, containsLineWith(lines, "HEADS-UP"), "HEADS-UP should NOT fire below the threshold; got:\n%s", strings.Join(lines, "\n")) } // staleConn implements net.Conn and returns io.EOF on Read after // a fixed number of writes succeed. Used to simulate a server (or // NAT) silently closing a conn while it sat in the idle pool. type staleConn struct { mu sync.Mutex closed bool } func (c *staleConn) Read(_ []byte) (int, error) { return 0, io.EOF } func (c *staleConn) Write(p []byte) (int, error) { // Pretend the write succeeded so the Transport gets past // WroteHeaders/WroteRequest before noticing the conn is dead. return len(p), nil } func (c *staleConn) Close() error { c.mu.Lock() c.closed = true c.mu.Unlock() return nil } func (*staleConn) LocalAddr() net.Addr { return &net.TCPAddr{} } func (*staleConn) RemoteAddr() net.Addr { return &net.TCPAddr{} } func (*staleConn) SetDeadline(time.Time) error { return nil } func (*staleConn) SetReadDeadline(time.Time) error { return nil } func (*staleConn) SetWriteDeadline(time.Time) error { return nil } // TestRuntime_Trace_StaleConnRealRoundTrip exercises the full // SubmitContext path with a Transport whose Dial returns a conn // that EOFs on read. The round-trip fails; trace output should // carry a FAILED summary line — but the annotation block does NOT // fire because the conn is fresh (Reused=false, no idle history). // This is the boundary case: same symptom, but the diagnostic // only fires when the data on the GotConn event justifies it. func TestRuntime_Trace_StaleConnRealRoundTrip(t *testing.T) { transport := &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return &staleConn{}, nil }, DisableKeepAlives: true, } rec := &recordingLogger{} rt := New("example.com", "/", []string{schemeHTTP}) rt.Transport = transport rt.Trace = true rt.SetLogger(rec) rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) _, err := rt.Submit(&runtime.ClientOperation{ ID: testOpGetOk, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return struct{}{}, nil }), }) require.Error(t, err) lines := rec.snapshot() assert.True(t, containsLineWith(lines, "Summary:", "FAILED"), "expected FAILED summary; got:\n%s", strings.Join(lines, "\n")) // Fresh conn → no HEADS-UP / issue-#336 annotation. This is // the correct behaviour: the diagnostic only fires when the // connection's reuse history points the finger. assert.False(t, containsLineWith(lines, "issue-#336"), "issue-#336 must NOT fire on a fresh conn even if it EOFs; got:\n%s", strings.Join(lines, "\n")) } // =============================================================== // TLS diagnostic mode // =============================================================== // generateTestCert builds a single self-signed ECDSA leaf for use // in TLS tests. notBefore/notAfter let the caller forge expiry; // dnsNames is the SAN list embedded in the cert. func generateTestCert(t *testing.T, notBefore, notAfter time.Time, dnsNames ...string) (tls.Certificate, *x509.Certificate) { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) require.NoError(t, err) tmpl := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{CommonName: "trace-test"}, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, DNSNames: dnsNames, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, } der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) require.NoError(t, err) leaf, err := x509.ParseCertificate(der) require.NoError(t, err) keyBytes, err := x509.MarshalECPrivateKey(key) require.NoError(t, err) keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) cert, err := tls.X509KeyPair(certPEM, keyPEM) require.NoError(t, err) return cert, leaf } // runTLSFailureSubmit drives a Submit against a TLS server whose // cert configuration is set by `serverCert`, with a client // configured via `clientTLS`. Returns the trace lines captured. func runTLSFailureSubmit(t *testing.T, serverCert tls.Certificate, clientTLS *tls.Config) []string { t.Helper() server := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusOK) })) server.TLS = &tls.Config{Certificates: []tls.Certificate{serverCert}} server.StartTLS() defer server.Close() hu, err := url.Parse(server.URL) require.NoError(t, err) rec := &recordingLogger{} rt := New(hu.Host, "/", []string{schemeHTTPS}) rt.Transport = &http.Transport{TLSClientConfig: clientTLS} rt.Trace = true rt.SetLogger(rec) rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) _, _ = rt.Submit(&runtime.ClientOperation{ ID: testOpGetOk, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return struct{}{}, nil }), }) return rec.snapshot() } // TestRuntime_Trace_TLS_UnknownAuthority: the default httptest TLS // server uses a self-signed cert that's not in the system trust // store, so a vanilla client gets x509.UnknownAuthorityError. The // diagnostic block should classify it as cert-chain / unknown-CA. func TestRuntime_Trace_TLS_UnknownAuthority(t *testing.T) { server := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusOK) })) defer server.Close() hu, err := url.Parse(server.URL) require.NoError(t, err) rec := &recordingLogger{} rt := New(hu.Host, "/", []string{schemeHTTPS}) rt.Trace = true rt.SetLogger(rec) rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) _, err = rt.Submit(&runtime.ClientOperation{ ID: testOpGetOk, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return struct{}{}, nil }), }) require.Error(t, err) lines := rec.snapshot() assert.True(t, containsLineWith(lines, "TLS DIAGNOSTIC"), "missing TLS diagnostic header; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "axis: cert-chain"), "missing cert-chain axis tag; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "unknown-CA"), "missing unknown-CA reason; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "SystemCertPool"), "missing trust-store note; got:\n%s", strings.Join(lines, "\n")) } // TestRuntime_Trace_TLS_ExpiredCert forges a cert with NotAfter in // the past; the diagnostic should report reason=expired plus the // expiry timestamps. func TestRuntime_Trace_TLS_ExpiredCert(t *testing.T) { now := time.Now() cert, _ := generateTestCert(t, now.Add(-48*time.Hour), now.Add(-1*time.Hour), "127.0.0.1", ) lines := runTLSFailureSubmit(t, cert, nil) assert.True(t, containsLineWith(lines, "TLS DIAGNOSTIC"), "missing TLS diagnostic header; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "axis: cert-chain"), "missing cert-chain axis tag; got:\n%s", strings.Join(lines, "\n")) // On modern Go, an expired self-signed cert is reported as // either x509.Expired or, when chain-building gives up first, // UnknownAuthorityError. Accept either — both come from the // same root cause. assert.True(t, containsLineWith(lines, "reason:", "expired") || containsLineWith(lines, "unknown-CA"), "missing expired/unknown-CA reason; got:\n%s", strings.Join(lines, "\n")) } // TestRuntime_Trace_TLS_HostnameMismatch: dial 127.0.0.1 but serve // a cert whose SANs only cover example.com. The handshake itself // succeeds; verification on the client side then trips // x509.HostnameError. The trust setup uses the leaf as its own // RootCA so the failure mode is hostname, not unknown-CA. func TestRuntime_Trace_TLS_HostnameMismatch(t *testing.T) { now := time.Now() cert, leaf := generateTestCert(t, now.Add(-1*time.Hour), now.Add(24*time.Hour), "example.com", ) pool := x509.NewCertPool() pool.AddCert(leaf) clientTLS := &tls.Config{ MinVersion: tls.VersionTLS12, RootCAs: pool, ServerName: "wronghost", // forces hostname check at "wronghost" } lines := runTLSFailureSubmit(t, cert, clientTLS) assert.True(t, containsLineWith(lines, "TLS DIAGNOSTIC"), "missing TLS diagnostic header; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "axis: cert-chain"), "missing cert-chain axis tag; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "hostname mismatch"), "missing hostname-mismatch reason; got:\n%s", strings.Join(lines, "\n")) } // TestRuntime_Trace_TLS_ProtocolVersionMismatch: client pins // MinVersion=TLS 1.3, server caps at MaxVersion=TLS 1.2. The // negotiation fails with a protocol-version alert / error. func TestRuntime_Trace_TLS_ProtocolVersionMismatch(t *testing.T) { now := time.Now() cert, leaf := generateTestCert(t, now.Add(-1*time.Hour), now.Add(24*time.Hour), "127.0.0.1", ) pool := x509.NewCertPool() pool.AddCert(leaf) server := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusOK) })) server.TLS = &tls.Config{ //nolint:gosec // Deliberately caps at TLS 1.2 to force a protocol-version mismatch with the TLS-1.3-only client. Certificates: []tls.Certificate{cert}, MaxVersion: tls.VersionTLS12, } server.StartTLS() defer server.Close() hu, err := url.Parse(server.URL) require.NoError(t, err) rec := &recordingLogger{} rt := New(hu.Host, "/", []string{schemeHTTPS}) rt.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS13, RootCAs: pool, }, } rt.Trace = true rt.SetLogger(rec) rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) _, err = rt.Submit(&runtime.ClientOperation{ ID: testOpGetOk, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return struct{}{}, nil }), }) require.Error(t, err) lines := rec.snapshot() assert.True(t, containsLineWith(lines, "TLS DIAGNOSTIC"), "missing TLS diagnostic header; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "axis: protocol-version"), "expected protocol-version axis; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "TLS 1.3"), "expected client-offered TLS 1.3; got:\n%s", strings.Join(lines, "\n")) } // TestRuntime_Trace_TLS_HappyPath verifies the happy-path summary // (negotiated version, cipher, server, expiry) on a successful // TLS handshake. No diagnostic block should appear. func TestRuntime_Trace_TLS_HappyPath(t *testing.T) { now := time.Now() cert, leaf := generateTestCert(t, now.Add(-1*time.Hour), now.Add(24*time.Hour), "127.0.0.1", ) pool := x509.NewCertPool() pool.AddCert(leaf) clientTLS := &tls.Config{ MinVersion: tls.VersionTLS12, RootCAs: pool, } lines := runTLSFailureSubmit(t, cert, clientTLS) // Despite the helper name, this case is the happy path — no // TLS diagnostic block, but TLSHandshakeDone reports the // negotiated parameters. assert.False(t, containsLineWith(lines, "TLS DIAGNOSTIC"), "happy path must not emit a TLS diagnostic block; got:\n%s", strings.Join(lines, "\n")) assert.True(t, containsLineWith(lines, "TLSHandshakeDone(", "cipher=", "expires="), "happy path TLSHandshakeDone should report cipher and expiry; got:\n%s", strings.Join(lines, "\n")) } func TestRuntime_Trace_OffEmitsNothing(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Set(runtime.HeaderContentType, runtime.JSONMime) rw.WriteHeader(http.StatusOK) _, _ = rw.Write([]byte(`{"ok":true}`)) })) defer server.Close() hu, err := url.Parse(server.URL) require.NoError(t, err) rec := &recordingLogger{} rt := New(hu.Host, "/", []string{schemeHTTP}) // rt.Trace stays false; rt.Debug also false → no output expected. rt.Debug = false rt.SetLogger(rec) rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) _, err = rt.Submit(&runtime.ClientOperation{ ID: testOpGetOk, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return struct{}{}, nil }), }) require.NoError(t, err) assert.Empty(t, rec.snapshot(), "no trace output expected when Trace=false") } go-openapi-runtime-decad8f/client/httptrace_tls.go000066400000000000000000000274141520232310000225520ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "crypto/tls" "crypto/x509" "errors" "fmt" "net/http" "strings" "time" ) // TLS alert codes used by the diagnostic to classify handshake // failures. The crypto/tls package does not export named constants // for individual alerts, so we declare the ones we care about. // Values are from RFC 8446 §6 (the TLS 1.3 alert protocol; the // numbering is shared with earlier TLS versions for these alerts). // // The `err`-prefixed names satisfy the errname linter — tls.AlertError // implements error, so these are sentinel errors. const ( errTLSAlertHandshakeFailure tls.AlertError = 40 errTLSAlertProtocolVersion tls.AlertError = 70 ) // introspectTLSConfig returns the *tls.Config of the http.Transport // that will run a request, when reachable, or nil otherwise. // // Reachable means the client's Transport is an *http.Transport // (the default and most common case). Custom transports — wrappers // around the default, or entirely user-provided — break introspection; // the TLS diagnostic falls back to "configured: not introspectable" // in that case. // // A nil client (zero value) or nil Transport falls through to // [http.DefaultTransport], whose TLSClientConfig is also nil; the // function returns nil and the diagnostic reports defaults. func introspectTLSConfig(client *http.Client) *tls.Config { if client == nil { return nil } transport := client.Transport if transport == nil { transport = http.DefaultTransport } t, ok := transport.(*http.Transport) if !ok { return nil } return t.TLSClientConfig } // emitTLSDiagnostic renders the failure-mode TLS diagnostic block. // Called from [traceSession.onTLSHandshakeDone] when err != nil. // // The block covers three axes (per the plan): // // 1. Protocol-version negotiation — detected from // [errTLSAlertProtocolVersion] or a "protocol version" substring. // 2. Cipher-suite negotiation — detected from // [errTLSAlertHandshakeFailure] when the user pinned CipherSuites. // 3. Certificate-chain validity — detected from // [x509.CertificateInvalidError], [x509.UnknownAuthorityError] // or [x509.HostnameError]. // // When none of the specific axes match, a generic fallback emits // the raw error and whatever inspectable config the session holds. func (s *traceSession) emitTLSDiagnostic(state tls.ConnectionState, err error) { s.emitf("# TLS DIAGNOSTIC") // tlsAxisGeneric is handled by the default branch. switch axis := classifyTLSError(err); axis { case tlsAxisProtocolVersion: s.diagnoseProtocolVersion(state, err) case tlsAxisCipher: s.diagnoseCipher(err) case tlsAxisCertChain: s.diagnoseCertChain(err) default: s.diagnoseTLSGeneric(err) } } // tlsAxis is the diagnostic dimension a TLS handshake error maps // to. Axes are mutually exclusive at classification time. type tlsAxis int const ( tlsAxisGeneric tlsAxis = iota tlsAxisProtocolVersion tlsAxisCipher tlsAxisCertChain ) // classifyTLSError maps a TLS handshake error to one of the // diagnostic axes. The ordering matters: cert-chain errors win // over the generic handshake_failure alert because the alert is // what the server sends back, but the local error type carries // the more specific reason. func classifyTLSError(err error) tlsAxis { if err == nil { return tlsAxisGeneric } // Cert-chain errors are the most specific local diagnostic // and should be reported even if a generic alert is also // present in the chain. var certInvalid x509.CertificateInvalidError if errors.As(err, &certInvalid) { return tlsAxisCertChain } var unknownAuth x509.UnknownAuthorityError if errors.As(err, &unknownAuth) { return tlsAxisCertChain } var hostnameErr x509.HostnameError if errors.As(err, &hostnameErr) { return tlsAxisCertChain } // TLS alert classification. var alert tls.AlertError if errors.As(err, &alert) { switch alert { case errTLSAlertProtocolVersion: return tlsAxisProtocolVersion case errTLSAlertHandshakeFailure: return tlsAxisCipher } } // Fall back on substring detection for protocol-version // failures that arrive via the local error path rather than // a server-side alert (e.g. when the client refuses the // server's offered version). msg := err.Error() if strings.Contains(msg, "protocol version") || strings.Contains(msg, "unsupported protocol") { return tlsAxisProtocolVersion } return tlsAxisGeneric } // --------------------------------------------------------------- // Axis renderers // --------------------------------------------------------------- func (s *traceSession) diagnoseProtocolVersion(state tls.ConnectionState, err error) { s.emitf("# axis: protocol-version") s.emitf("# error: %v", err) configuredMin, configuredMax := configuredVersionRange(s.tlsCfg) s.emitf("# client offered: TLS %s — TLS %s", tlsVersionName(configuredMin), tlsVersionName(configuredMax)) if state.Version != 0 { s.emitf("# negotiated up to: TLS %s", tlsVersionName(state.Version)) } s.emitf("# suggested: widen TLSClientOptions.MinVersion/MaxVersion,") s.emitf("# or pin to a version the server speaks.") } func (s *traceSession) diagnoseCipher(err error) { s.emitf("# axis: cipher-suite") s.emitf("# error: %v", err) if s.tlsCfg != nil && len(s.tlsCfg.CipherSuites) > 0 { s.emitf("# client configured: [%s]", strings.Join(cipherSuiteNames(s.tlsCfg.CipherSuites), ", ")) s.emitf("# server set: not exposed by Go stdlib") s.emitf("# (capture with: openssl s_client -cipher ALL)") s.emitf("# suggested: drop the explicit CipherSuites restriction,") s.emitf("# or align it with the server's policy.") return } // No client-side restriction. The handshake_failure alert // is generic; without more info we can only surface the // fact and suggest investigation. s.emitf("# client configured: defaults (no CipherSuites restriction)") s.emitf("# note: alert 40 is generic; the server may have rejected") s.emitf("# the handshake for a non-cipher reason. Try") s.emitf("# openssl s_client to capture details.") } func (s *traceSession) diagnoseCertChain(err error) { s.emitf("# axis: cert-chain") var certInvalid x509.CertificateInvalidError if errors.As(err, &certInvalid) { s.diagnoseCertInvalid(certInvalid) return } var unknownAuth x509.UnknownAuthorityError if errors.As(err, &unknownAuth) { s.diagnoseUnknownAuthority(unknownAuth) return } var hostnameErr x509.HostnameError if errors.As(err, &hostnameErr) { s.diagnoseHostnameMismatch(hostnameErr) return } // Defensive: should not happen — classifyTLSError already // matched one of the three. s.emitf("# error: %v", err) } func (s *traceSession) diagnoseCertInvalid(certInvalid x509.CertificateInvalidError) { cert := certInvalid.Cert s.emitf("# reason: %s", certInvalidReasonName(certInvalid.Reason)) switch certInvalid.Reason { case x509.Expired: s.emitf("# leaf: subject=%s", cert.Subject) s.emitf("# NotBefore=%s", cert.NotBefore.UTC().Format(time.RFC3339)) s.emitf("# NotAfter=%s", cert.NotAfter.UTC().Format(time.RFC3339)) s.emitf("# now=%s", time.Now().UTC().Format(time.RFC3339)) delta := time.Since(cert.NotAfter).Round(time.Hour) s.emitf("# expired %s ago", delta) s.emitf("# suggested: renew the server cert.") case x509.NameMismatch, x509.CANotAuthorizedForThisName: s.emitf("# leaf: subject=%s", cert.Subject) s.emitf("# DNS SANs=%v", cert.DNSNames) s.emitf("# suggested: set TLSClientOptions.ServerName to match") s.emitf("# one of the cert SANs, or fix the cert.") default: // Less-common reasons render via the default branch (issuer + NotAfter dump). s.emitf("# leaf: subject=%s, issuer=%s", cert.Subject, cert.Issuer) s.emitf("# NotBefore=%s", cert.NotBefore.UTC().Format(time.RFC3339)) s.emitf("# NotAfter=%s", cert.NotAfter.UTC().Format(time.RFC3339)) s.emitf("# error: %v", certInvalid) } } func (s *traceSession) diagnoseUnknownAuthority(unknownAuth x509.UnknownAuthorityError) { s.emitf("# reason: chain root not in trust store (unknown-CA)") if cert := unknownAuth.Cert; cert != nil { s.emitf("# offending: subject=%s", cert.Subject) s.emitf("# issuer=%s", cert.Issuer) s.emitf("# NotAfter=%s", cert.NotAfter.UTC().Format(time.RFC3339)) } trust := "SystemCertPool" if s.tlsCfg != nil && s.tlsCfg.RootCAs != nil { trust = "TLSClientOptions.CA (custom RootCAs)" } s.emitf("# trust store in use: %s", trust) s.emitf("# suggested: set TLSClientOptions.CA to a bundle that") s.emitf("# includes the issuing CA, or add it to the") s.emitf("# OS trust store.") } func (s *traceSession) diagnoseHostnameMismatch(hostnameErr x509.HostnameError) { s.emitf("# reason: hostname mismatch") s.emitf("# dialed: %s", hostnameErr.Host) if cert := hostnameErr.Certificate; cert != nil { s.emitf("# leaf: subject=%s", cert.Subject) s.emitf("# DNS SANs=%v", cert.DNSNames) s.emitf("# IP SANs=%v", cert.IPAddresses) } if s.tlsCfg != nil && s.tlsCfg.ServerName != "" { s.emitf("# TLSClientOptions.ServerName=%q", s.tlsCfg.ServerName) } s.emitf("# suggested: dial the hostname listed in the cert SANs,") s.emitf("# or set TLSClientOptions.ServerName to match.") } func (s *traceSession) diagnoseTLSGeneric(err error) { s.emitf("# axis: unclassified") s.emitf("# error: %v", err) if s.tlsCfg != nil { minV, maxV := configuredVersionRange(s.tlsCfg) s.emitf("# configured: MinVersion=TLS %s, MaxVersion=TLS %s", tlsVersionName(minV), tlsVersionName(maxV)) if s.tlsCfg.InsecureSkipVerify { s.emitf("# note: TLSClientOptions.InsecureSkipVerify=true — yet") s.emitf("# a TLS error still surfaced. Something deeper than") s.emitf("# certificate verification is failing.") } } } // --------------------------------------------------------------- // Helpers // --------------------------------------------------------------- // configuredVersionRange returns the effective (Min, Max) TLS // version range a client config negotiates. Zero values in the // stdlib config mean "use Go default", which is TLS 1.2 .. 1.3 in // modern Go. We materialize those defaults for display. func configuredVersionRange(cfg *tls.Config) (uint16, uint16) { const ( defaultMin = tls.VersionTLS12 defaultMax = tls.VersionTLS13 ) if cfg == nil { return defaultMin, defaultMax } minV := cfg.MinVersion if minV == 0 { minV = defaultMin } maxV := cfg.MaxVersion if maxV == 0 { maxV = defaultMax } return minV, maxV } func cipherSuiteNames(ids []uint16) []string { out := make([]string, 0, len(ids)) for _, id := range ids { out = append(out, tls.CipherSuiteName(id)) } return out } // certInvalidReasonName renders an x509.InvalidReason as a short // human-readable label. The stdlib does not expose a String() // method for these, so we keep a small table. // // Anything outside the listed cases falls through to the numeric default. func certInvalidReasonName(r x509.InvalidReason) string { switch r { case x509.NotAuthorizedToSign: return "not-authorized-to-sign" case x509.Expired: return "expired" case x509.CANotAuthorizedForThisName: return "ca-not-authorized-for-this-name" case x509.TooManyIntermediates: return "too-many-intermediates" case x509.IncompatibleUsage: return "incompatible-usage" case x509.NameMismatch: return "name-mismatch" case x509.NameConstraintsWithoutSANs: return "name-constraints-without-sans" case x509.TooManyConstraints: return "too-many-constraints" case x509.CANotAuthorizedForExtKeyUsage: return "ca-not-authorized-for-ext-key-usage" default: return fmt.Sprintf("invalid-reason-%d", r) } } go-openapi-runtime-decad8f/client/internal/000077500000000000000000000000001520232310000211475ustar00rootroot00000000000000go-openapi-runtime-decad8f/client/internal/request/000077500000000000000000000000001520232310000226375ustar00rootroot00000000000000go-openapi-runtime-decad8f/client/internal/request/escape_test.go000066400000000000000000000031521520232310000254660ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package request import ( "strings" "testing" "github.com/go-openapi/testify/v2/assert" ) // TestEscapeQuotes_StripsCRLF verifies that escapeQuotes neutralises // CR / LF in Content-Disposition parameter values, preventing // header-injection through attacker-influenced field names or // filenames. Mirrors the known stdlib gap golang/go#19038. // // Security scrub Lens 3 / L3.2. func TestEscapeQuotes_StripsCRLF(t *testing.T) { cases := []struct { name string in string want string }{ { name: "embedded CR", in: "file\rname.txt", want: "file_name.txt", }, { name: "embedded LF", in: "file\nname.txt", want: "file_name.txt", }, { name: "embedded CRLF", in: "file\r\nname.txt", want: "file__name.txt", }, { name: "CRLF + injected header", in: "evil.txt\r\nContent-Type: forged", want: "evil.txt__Content-Type: forged", }, { name: "no control chars", in: "regular.txt", want: "regular.txt", }, { name: "still escapes quote", in: `with"quote.txt`, want: `with\"quote.txt`, }, { name: "still escapes backslash", in: `with\backslash.txt`, want: `with\\backslash.txt`, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := escapeQuotes(c.in) assert.Equal(t, c.want, got) // Belt-and-braces: result must contain no literal CR/LF // regardless of how the input was assembled. assert.False(t, strings.ContainsAny(got, "\r\n"), "output retained a CR or LF: %q", got) }) } } go-openapi-runtime-decad8f/client/internal/request/request.go000066400000000000000000000742071520232310000246700ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package request import ( "bytes" "context" "errors" "fmt" "io" "log" "mime" "mime/multipart" "net/http" "net/textproto" "net/url" "os" "path" "path/filepath" "strings" "time" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" ) var _ runtime.ClientRequest = new(Request) // ensure compliance to the interface // Request represents a swagger client request. // It binds parameters to a HTTP request. // // The main purpose of this struct is to hide the machinery of adding OpenAPI v2 parameters to a transport request. // // A generated client only implements what is necessary to turn a parameter into a valid value for these methods. // // There is no parameter validation here, it is assumed to be used after a spec has been validated. // // # Request binding // // The binding of parameters is carried out by method [Request.BuildHTTPContext]. // // It analyzes parameters, which may come in different flavors: // // - a file or multipart form containing a file // - a body which is a [io.Reader] // - a buffered body (regular schema body, including urlencoded form) // // In all cases, we may also have query or path parameters encoded in the URL, or header parameters. // // The result is a [http.Request], with the following properties: // // - file, multipart form or [io.Reader] body: a streaming request with an attached go routine that consumes the [io.Reader]. // - buffered body: a simple request // // The caller passes the parent [context.Context] to [Request.BuildHTTPContext] and receives back a cancel // function to release the resources held by the derived request context once the response is consumed. // // # Authentication // // Authentication is built in the request by using a [runtime.ClientAuthInfoWriter]. // This helper may need to inspect the body of the request before sending authentication info. // To cover that case, streaming bodies use a copy of the body [io.Reader] for the [runtime.ClientAuthInfoWriter] // to consume if it wants to. // // # Content negotiation // // The [Request] detects `multipart/form-data` to switch to streamed request. // // `application/x-www-form-urlencoded` is also honored, even for file parameters, which are not streamed in this case. // File parameters default behavior is `multipart/form-data`. // // The natural way to define the `Content-Type` header is to use the `contentType` parameter to switch to the map of // available body producers. // // For buffered requests, this setting override any `Content-Type` header possibly set by calling [Request.SetHeaderParam]. // // For streamed requests, users may want more flexibility, as we enter custom territory, with use-cases not supported by OpenAPI v2. // // The `Content-Type` header of a streamed request is defined using the following sequence: // // 1. if the caller sets an explicit value already in header — the user set it via // [Request.SetHeaderParam] during WriteToRequest, and we treat that as an intentional escape hatch // 2. use payload's [runtime.ContentTyper] declaration (in this case, the produced payload knows its content type) // 3. use `application/octet-stream` if it is available in the registered producers // 4. otherwise set the picker's mediaType // // For multi-part requests, the content type of each part is auto-detected using the following sequence: // // 1. use [runtime.ContentTyper] declaration (in this case, the file payload knows its content type) // 2. use [http.DetectContentType] on the first 512 bytes of the file // // # Concurrency // // A [Request] is a disposable object that is NOT intended to be reused or called concurrently. // // # Future evolutions // // There might be other similar structs that convert to other transports. type Request struct { pathPattern string method string writer runtime.ClientRequestWriter pathParams map[string]string header http.Header query url.Values formFields url.Values fileFields map[string][]runtime.NamedReadCloser payload any // consumes carries the operation's full ConsumesMediaTypes list so // that buildHTTP — which runs after the writer populates the payload // — can apply payload-aware fallback rules (see streamFallbackMime). // // This is set by Runtime.createHttpRequest. consumes []string timeout time.Duration buf *bytes.Buffer getBody func(r *Request) []byte } // New creates a new http client [Request] to handle OpenAPI v2 parameters. func New(method, pathPattern string, writer runtime.ClientRequestWriter) *Request { return &Request{ pathPattern: pathPattern, method: method, writer: writer, header: make(http.Header), query: make(url.Values), timeout: 0, getBody: getRequestBuffer, } } // GetMethod yields the method being used. func (r *Request) GetMethod() string { return r.method } // GetPath yields the URL path being used. func (r *Request) GetPath() string { pth := r.pathPattern for k, v := range r.pathParams { pth = strings.ReplaceAll(pth, "{"+k+"}", v) } return pth } // GetBody returns the request body, if any. // // For streaming requests, this is a copy of the original [io.Reader]. func (r *Request) GetBody() []byte { return r.getBody(r) } // SetHeaderParam adds a header parameter to the request. // // The header key is always canonicalized. // // - when there is only 1 value provided, it will set it. // - when there are several values provided, it will add all of those (no overriding). func (r *Request) SetHeaderParam(name string, values ...string) error { if r.header == nil { r.header = make(http.Header) } r.header[http.CanonicalHeaderKey(name)] = values return nil } // GetHeaderParams returns all headers currently set for the request. func (r *Request) GetHeaderParams() http.Header { return r.header } // SetQueryParam adds a query parameter to the request. // // - when there is only 1 value provided, it will set it. // - when there are several values provided, it will add all of those (no overriding). func (r *Request) SetQueryParam(name string, values ...string) error { if r.query == nil { r.query = make(url.Values) } r.query[name] = values return nil } // GetQueryParams returns a copy of all query params currently set for the request. func (r *Request) GetQueryParams() url.Values { result := make(url.Values, len(r.query)) for key, values := range r.query { result[key] = append([]string{}, values...) } return result } // SetFormParam adds a form param to the request. // // - when there is only 1 value provided, it will set it. // - when there are several values provided, it will add all of those (no overriding). func (r *Request) SetFormParam(name string, values ...string) error { if r.formFields == nil { r.formFields = make(url.Values) } r.formFields[name] = values return nil } // SetPathParam adds a path param to the request. func (r *Request) SetPathParam(name string, value string) error { if r.pathParams == nil { r.pathParams = make(map[string]string) } r.pathParams[name] = value return nil } // SetFileParam adds a file parameter to the request. // // Files must implement [runtime.NamedReadCloser]. // // [runtime.File] is proposed as the default concrete implementation. func (r *Request) SetFileParam(name string, files ...runtime.NamedReadCloser) error { for _, file := range files { if actualFile, ok := file.(*os.File); ok { fi, err := os.Stat(actualFile.Name()) if err != nil { return err } if fi.IsDir() { return fmt.Errorf("%q is a directory, only files are supported", file.Name()) } } } if r.fileFields == nil { r.fileFields = make(map[string][]runtime.NamedReadCloser) } if r.formFields == nil { r.formFields = make(url.Values) } r.fileFields[name] = files return nil } // GetFileParam yields all file parameters. func (r *Request) GetFileParam() map[string][]runtime.NamedReadCloser { return r.fileFields } // SetBodyParam sets a body parameter on the request. // // This does not yet serialize the object: actual serialization happens as late as possible. func (r *Request) SetBodyParam(payload any) error { r.payload = payload return nil } // GetBodyParam returns the body payload. func (r *Request) GetBodyParam() any { return r.payload } // GetTimeout sets the timeout for a request. func (r *Request) GetTimeout() time.Duration { return r.timeout } // SetTimeout sets the timeout for a request. func (r *Request) SetTimeout(timeout time.Duration) error { r.timeout = timeout return nil } // SetConsumes sets the list of registered consumed content for a request. func (r *Request) SetConsumes(consumers []string) { r.consumes = consumers } // BuildHTTPContext binds the request parameters and returns a ready-to-send [http.Request]. // // Dispatch picks one of two end-to-end builders based on whether: // // - the body source is a stream (multipart pipe or stream payload) // - or a buffer (urlencoded form, producer output, or no body) // // It starts by writing the request, then proceed with adding authentication, // then finally assembling URL or header parameters. // // The split mirrors the auth question: streaming bodies require a lazy body-copy closure during [AuthenticateRequest], // whereas buffered bodies do not. // // The returned [http.Request] carries a context derived from parentCtx that: // // - inherits any deadline or cancellation already set on parentCtx; // - additionally honors the per-request timeout set via [Request.SetTimeout] // (the [runtime.ClientRequestWriter] may override the runtime default during // WriteToRequest, which is why the derivation happens here rather than // at the call site). // // The returned cancel must be invoked by the caller (typically deferred) // once the response has been fully read; otherwise resources held by the // derived context — including any timeout timer — are leaked. // // On error the cancel is invoked internally and a no-op cancel is returned, // so callers can defer cancel unconditionally. func (r *Request) BuildHTTPContext(parentCtx context.Context, mediaType, basePath string, producers map[string]runtime.Producer, registry strfmt.Registry, auth runtime.ClientAuthInfoWriter, ) (*http.Request, context.CancelFunc, error) { if err := r.writer.WriteToRequest(r, registry); err != nil { return nil, noop, err } ctx, cancel := deriveRequestContext(parentCtx, r.timeout) r.buf = bytes.NewBuffer(nil) var ( httpReq *http.Request err error ) if r.usesStreamingBody(mediaType) { httpReq, err = r.buildStreamingRequest(ctx, mediaType, basePath, producers, registry, auth) } else { httpReq, err = r.buildBufferedRequest(ctx, mediaType, basePath, producers, registry, auth) } if err != nil { cancel() return nil, noop, err } return httpReq, cancel, nil } func noop() {} // deriveRequestContext returns a child of parent bounded by timeout. // If timeout == 0 the child is only canceled when the caller invokes // cancel; any deadline already on parent is preserved. If timeout > 0 // the child uses the shortest of timeout and parent's existing deadline. func deriveRequestContext(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { if timeout == 0 { return context.WithCancel(parent) } return context.WithTimeout(parent, timeout) } // usesStreamingBody reports whether the request body must be assembled // as a stream (an io.Pipe for multipart, or the payload's own reader // for stream payloads). // // The complementary case is a fully buffered body in r.buf — urlencoded form, producer output, or no body at all. func (r *Request) usesStreamingBody(mediaType string) bool { if (len(r.formFields) > 0 || len(r.fileFields) > 0) && r.isMultipart(mediaType) { return true } if r.payload != nil { if _, ok := r.payload.(io.Reader); ok { return true } } return false } func (r *Request) isMultipart(mediaType string) bool { // Strip media-type parameters before comparing: callers may legally // pass `multipart/form-data; boundary=…` or // `application/x-www-form-urlencoded; charset=utf-8` per RFC 7231, // and a bare-string compare would route those to the wrong flow. // // mime.ParseMediaType lowercases the type/subtype and is // case-insensitive on input, so plain == against our (lowercase) // constants is sufficient on the happy path. base, _, err := mime.ParseMediaType(mediaType) if err != nil { // Malformed mediaType: only the file-presence shortcut can // fire — by definition we cannot recognize either canonical // form mime in unparseable input. return len(r.fileFields) > 0 } // An explicit application/x-www-form-urlencoded choice is honored even when // file fields are present: the spec allows files to travel as URL-encoded // form values, although it does not stream and is discouraged. Without this // short-circuit, picking urlencoded with files would silently fall back to // multipart and emit an inconsistent Content-Type. if base == runtime.URLencodedFormMime { return false } if len(r.fileFields) > 0 { return true } return base == runtime.MultipartFormMime } // buildBufferedRequest assembles a request whose body is fully // buffered in r.buf before AuthenticateRequest runs — urlencoded form, // producer-serialized payload, or no body. // // Auth is trivial in this flow because the buffer is already populated when the auth helper // asks for the body via r.GetBody(). func (r *Request) buildBufferedRequest(ctx context.Context, mediaType, basePath string, producers map[string]runtime.Producer, registry strfmt.Registry, auth runtime.ClientAuthInfoWriter, ) (*http.Request, error) { var body io.Reader var err error switch { case len(r.formFields) > 0 || len(r.fileFields) > 0: body, err = r.writeURLEncodedBody(mediaType) case r.payload != nil: body, err = r.writeNonStreamPayload(mediaType, producers) } if err != nil { return nil, err } if runtime.CanHaveBody(r.method) && body != nil && r.header.Get(runtime.HeaderContentType) == "" { r.header.Set(runtime.HeaderContentType, mediaType) } if auth != nil { if err := auth.AuthenticateRequest(r, registry); err != nil { return nil, err } } return r.assembleRequest(ctx, basePath, body) } // buildStreamingRequest assembles a request whose body is a stream — // either an io.Pipe filled by the multipart goroutine, or the // payload's own io.Reader. // // AuthenticateRequest consumes the body lazily through the getBody closure installed by // applyAuthWithBodyCopy, which buffers the stream into r.buf so the http.Request can use the buffered copy. // // On any error path before the http.Request takes ownership of body, we close the body to release // the underlying resource. // // For multipart this unblocks the spawned writer goroutine // (it would otherwise park forever on pw.Write with no reader). // // For stream payloads it closes the user-provided io.ReadCloser. func (r *Request) buildStreamingRequest(ctx context.Context, mediaType, basePath string, producers map[string]runtime.Producer, registry strfmt.Registry, auth runtime.ClientAuthInfoWriter, ) (req *http.Request, retErr error) { var body io.Reader if len(r.formFields) > 0 || len(r.fileFields) > 0 { body = r.writeMultipartBody(ctx, mediaType) } else { body = r.writeStreamPayload(mediaType, producers) } defer func() { if retErr == nil { return } if c, ok := body.(io.Closer); ok { _ = c.Close() } }() if runtime.CanHaveBody(r.method) && body != nil && r.header.Get(runtime.HeaderContentType) == "" { r.header.Set(runtime.HeaderContentType, mediaType) } body, err := r.applyAuthWithBodyCopy(auth, body, registry) if err != nil { return nil, err } return r.assembleRequest(ctx, basePath, body) } // assembleRequest is the shared tail of both flows: build the URL // path, create the http.Request, merge static query parameters, and // finalize headers/query. func (r *Request) assembleRequest(ctx context.Context, basePath string, body io.Reader) (*http.Request, error) { urlPath, staticQueryParams, err := r.resolveURLPath(basePath) if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, r.method, urlPath, body) if err != nil { return nil, err } if err := r.mergeStaticQuery(staticQueryParams); err != nil { return nil, err } req.URL.RawQuery = r.query.Encode() req.Header = r.header return req, nil } // resolveURLPath builds the final url path string and returns the static // query parameters extracted from basePath and r.pathPattern. // // Static query parameters from the path pattern take precedence over those // from the base path; merging with r.query is the caller's responsibility // (see [request.mergeStaticQuery]). // // The path is assembled from basePath + pathPattern with path-param // substitution and trailing-slash preservation when the original // pathPattern carried one. func (r *Request) resolveURLPath(basePath string) (string, url.Values, error) { basePathURL, err := url.Parse(basePath) if err != nil { return "", nil, err } staticQueryParams := basePathURL.Query() pathPatternURL, err := url.Parse(r.pathPattern) if err != nil { return "", nil, err } for name, values := range pathPatternURL.Query() { if _, present := staticQueryParams[name]; present { staticQueryParams.Del(name) } for _, value := range values { staticQueryParams.Add(name, value) } } // path.Join strips trailing slashes; reinstate one whenever the // pathPattern carried it, including the bare-root case ("/" under a // non-empty basePath, which path.Join would collapse to "/basepath"). // The HasSuffix check on urlPath keeps the rewrite idempotent and // avoids producing "//" when basePath is "/" or empty. reinstateSlash := strings.HasSuffix(pathPatternURL.Path, "/") urlPath := path.Join(basePathURL.Path, pathPatternURL.Path) for k, v := range r.pathParams { urlPath = strings.ReplaceAll(urlPath, "{"+k+"}", url.PathEscape(v)) } if reinstateSlash && !strings.HasSuffix(urlPath, "/") { urlPath += "/" } return urlPath, staticQueryParams, nil } // applyAuthWithBodyCopy runs auth.AuthenticateRequest for the // streaming flow, where the http.Request body is a pipe or a payload // reader rather than r.buf. If AuthenticateRequest asks for the body // via r.GetBody(), the lazy closure copies the stream into r.buf on // demand and reassigns body to r.buf so the post-auth source passed // to http.NewRequestWithContext is the buffered copy. // // The closure is registered lazily because there is no way to know // ahead of time whether AuthenticateRequest will read the body. // // On error precedence: a copy error is reported in preference to the // AuthenticateRequest error, because a mis-read body may have // interfered with auth. // // No-op when auth is nil; returns body unchanged. func (r *Request) applyAuthWithBodyCopy(auth runtime.ClientAuthInfoWriter, body io.Reader, registry strfmt.Registry) (io.Reader, error) { if auth == nil { return body, nil } var copyErr error var copied bool r.getBody = func(r *Request) []byte { if copied { return getRequestBuffer(r) } defer func() { copied = true }() if _, copyErr = io.Copy(r.buf, body); copyErr != nil { return nil } if closer, ok := body.(io.ReadCloser); ok { if copyErr = closer.Close(); copyErr != nil { return nil } } body = r.buf return getRequestBuffer(r) } authErr := auth.AuthenticateRequest(r, registry) // On error we return body alongside the error so the caller's // cleanup defer (in buildStreamingRequest) can close the // underlying pipe/stream. Caller treats body as ignorable when // err != nil per Go convention; the defer reads it via closure. if copyErr != nil { return body, fmt.Errorf("error copying the request body: %w", copyErr) } if authErr != nil { return body, authErr } return body, nil } // mergeStaticQuery overlays staticQuery onto r.query. On conflict r.query // wins — the parameters set by the client take precedence over the ones // extracted from basePath / pathPattern. func (r *Request) mergeStaticQuery(staticQuery url.Values) error { originalParams := r.GetQueryParams() for k, v := range staticQuery { if _, present := originalParams[k]; present { continue } if err := r.SetQueryParam(k, v...); err != nil { return err } } return nil } // writeURLEncodedBody serializes form fields (and any file fields, per // Swagger 2.0 fallback semantics) into r.buf as // application/x-www-form-urlencoded. Sets Content-Type to mediaType and // returns r.buf as the body source. // // Per Swagger 2.0, file form parameters can be sent under // application/x-www-form-urlencoded by including the file content as a // regular form-field value. The whole form is then percent-encoded as // usual. This buffers the entire payload and does not preserve a // per-file Content-Type — multipart/form-data is preferred when both // are advertised by the operation. func (r *Request) writeURLEncodedBody(mediaType string) (io.Reader, error) { r.header.Set(runtime.HeaderContentType, mediaType) values := url.Values{} for k, vs := range r.formFields { values[k] = append(values[k], vs...) } for fn, ff := range r.fileFields { for _, fi := range ff { data, ferr := io.ReadAll(fi) if cerr := fi.Close(); cerr != nil && ferr == nil { ferr = cerr } if ferr != nil { return nil, ferr } values.Add(fn, string(data)) } } r.buf.WriteString(values.Encode()) return r.buf, nil } // writeMultipartBody assembles a multipart/form-data body via an // io.Pipe. A goroutine streams form fields and files into the pipe // writer; the pipe reader is returned as the body. Sets Content-Type to // the multipart media type with the writer's boundary parameter. // // The goroutine owns the pipe writer's lifecycle: it closes the // multipart writer (flushing the closing boundary) and the pipe writer // when it finishes or hits an error. func (r *Request) writeMultipartBody(ctx context.Context, mediaType string) io.Reader { pr, pw := io.Pipe() mp := multipart.NewWriter(pw) r.header.Set(runtime.HeaderContentType, mangleContentType(mediaType, mp.Boundary())) go r.streamMultipartParts(ctx, mp, pw) return pr } // streamMultipartParts writes form fields then file fields to mp, // closing mp and pw when done. // // Errors are reported by closing pw with the error so the consumer of pr observes them on its next Read. // // Context cancellation is observed at iteration boundaries (between // fields and between files) and during file copy via a context-aware // reader. When ctx is canceled the pipe writer is closed with ctx.Err() // so the body consumer surfaces the cancellation as the read error. func (r *Request) streamMultipartParts(ctx context.Context, mp *multipart.Writer, pw *io.PipeWriter) { defer func() { mp.Close() pw.Close() }() for fn, v := range r.formFields { for _, vi := range v { if err := ctx.Err(); err != nil { _ = pw.CloseWithError(err) return } if err := mp.WriteField(fn, vi); err != nil { logClose(err, pw) return } } } defer func() { for _, ff := range r.fileFields { for _, ffi := range ff { ffi.Close() } } }() for fn, f := range r.fileFields { for _, fi := range f { if err := ctx.Err(); err != nil { _ = pw.CloseWithError(err) return } var fileContentType string if p, ok := fi.(runtime.ContentTyper); ok { fileContentType = p.ContentType() } else { // Need to read the data so that we can detect the content type const contentTypeBufferSize = 512 buf := make([]byte, contentTypeBufferSize) size, err := fi.Read(buf) if err != nil && !errors.Is(err, io.EOF) { logClose(err, pw) return } fileContentType = http.DetectContentType(buf) fi = runtime.NamedReader(fi.Name(), io.MultiReader(bytes.NewReader(buf[:size]), fi)) } // Create the MIME headers for the new part h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(fn), escapeQuotes(filepath.Base(fi.Name())))) h.Set("Content-Type", fileContentType) wrtr, err := mp.CreatePart(h) if err != nil { logClose(err, pw) return } if _, err := io.Copy(wrtr, &ctxReader{ctx: ctx, r: fi}); err != nil { logClose(err, pw) return } } } } // ctxReader wraps an [io.Reader] with a context check on each Read. Once // ctx is done, subsequent Reads return ctx.Err() instead of delegating // to the underlying reader. It does not preempt a Read already in flight // — that is the source's responsibility (e.g. *os.File honors Close from // another goroutine, network sources honor SetDeadline). type ctxReader struct { ctx context.Context //nolint:containedctx // io.Reader's Read method has no ctx parameter, so the wrapper must carry it on the struct r io.Reader } func (cr *ctxReader) Read(p []byte) (int, error) { if err := cr.ctx.Err(); err != nil { return 0, err } return cr.r.Read(p) } // writeStreamPayload handles a stream payload (io.Reader / // io.ReadCloser). The bytes flow through verbatim — no producer is // invoked. The wire Content-Type is resolved via setStreamContentType // (priority: existing header, payload's ContentTyper, // streamFallbackMime, mediaType). // // Caller must ensure r.payload satisfies io.Reader (see // [request.usesStreamingBody]). func (r *Request) writeStreamPayload(mediaType string, producers map[string]runtime.Producer) io.Reader { setStreamContentType(r.header, r.payload, mediaType, r.consumes, producers) if rdr, ok := r.payload.(io.ReadCloser); ok { return rdr } rdr, ok := r.payload.(io.Reader) if !ok { panic("internal error: payload expected to be an io.Reader") // guaranteed by earlier checks } return rdr } // writeNonStreamPayload runs the producer registered for mediaType // against r.payload, writing into r.buf. The Content-Type header // reflects the picker. // // SetHeaderParam("Content-Type", …) is intentionally NOT honored on // the producer path because the producer is dispatched off mediaType — // the wire header would otherwise misrepresent the body. // // The same reasoning applies to the form/multipart branch. func (r *Request) writeNonStreamPayload(mediaType string, producers map[string]runtime.Producer) (io.Reader, error) { r.header.Set(runtime.HeaderContentType, mediaType) producer := producers[mediaType] if err := producer.Produce(r.buf, r.payload); err != nil { return nil, err } return r.buf, nil } var quoter = strings.NewReplacer( "\\", "\\\\", `"`, "\\\"", "\r", "_", "\n", "_", ) // escapeQuotes escapes backslash and double-quote for embedding in a // quoted-string Content-Disposition parameter value, and rewrites // CR / LF to '_' to prevent header-injection through attacker-influenced // field names or filenames. // // RFC 7578 §4.2 limits parameter values to printable characters; this // is the conservative subset relevant to security (control characters // that would split the header line into a forged header or part). // Mirrors the known stdlib gap golang/go#19038. func escapeQuotes(s string) string { return quoter.Replace(s) } // setStreamContentType resolves and writes the wire Content-Type for a // stream payload (io.Reader / io.ReadCloser). Priority: // // 1. an explicit value already in header — the user set it via // SetHeaderParam during [ClientRequestWriter.WriteToRequest], and we treat that as an // intentional escape hatch; // 2. payload's [runtime.ContentTyper] declaration; // 3. [streamFallbackMime] (Stage-2 octet-stream upgrade); // 4. the picker's mediaType (passed in as the chain's terminal // fallback). // // Does not apply to non-stream payloads or to form/multipart bodies — // see the comment above the call site in [request.buildHTTP]. func setStreamContentType( header http.Header, payload any, mediaType string, candidates []string, producers map[string]runtime.Producer, ) { if header.Get(runtime.HeaderContentType) != "" { return } fallback := streamFallbackMime(mediaType, candidates, producers) header.Set(runtime.HeaderContentType, payloadContentType(payload, fallback)) } // payloadContentType returns the payload's declared content type when // it implements [runtime.ContentTyper] with a non-empty result, and // fallback otherwise. Mirrors the per-file convention already used for // multipart upload parts (see [request.buildHTTP] file-fields branch). func payloadContentType(payload any, fallback string) string { if t, ok := payload.(runtime.ContentTyper); ok { if ct := t.ContentType(); ct != "" { return ct } } return fallback } // streamFallbackMime selects a wire content-type for a stream payload // (io.Reader / io.ReadCloser) that has neither implemented // `ContentType() string` nor declared an explicit value. // // The picker (Stage 1) ran without seeing the payload, so its choice // may be wildly wrong for raw bytes — e.g. picking application/json // for a payload that is just a stream of opaque data. When the // candidate consumes list also offers application/octet-stream and // the runtime has an octet-stream producer registered, that's a // safer wire type than the picker's choice: it advertises "raw bytes" // rather than making a structural claim about the body. // // If octet-stream is unavailable in either the candidate list or the // producer set, the picker's choice is preserved. The wire header // then continues to misrepresent the body — but no correct // alternative exists and we cannot infer one without more // information from the caller. func streamFallbackMime(picked string, candidates []string, producers map[string]runtime.Producer) string { if strings.EqualFold(picked, runtime.DefaultMime) { return picked } for _, c := range candidates { if strings.EqualFold(c, runtime.DefaultMime) { if _, ok := producers[runtime.DefaultMime]; ok { return runtime.DefaultMime } } } return picked } func getRequestBuffer(r *Request) []byte { if r.buf == nil { return nil } return r.buf.Bytes() } func logClose(err error, pw *io.PipeWriter) { log.Println(err) closeErr := pw.CloseWithError(err) if closeErr != nil { log.Println(closeErr) } } func mangleContentType(mediaType, boundary string) string { _ = mediaType // reserved for future enhancement: honor caller-provided media type // Proposal for enhancement: honor caller's boundary if specified return "multipart/form-data; boundary=" + boundary } go-openapi-runtime-decad8f/client/internal/request/request_test.go000066400000000000000000001165031520232310000257230ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package request import ( "bytes" "context" "encoding/json" "encoding/xml" "errors" "io" "mime" "mime/multipart" "net/http" "net/url" "os" "path/filepath" "strings" "testing" "time" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" "github.com/go-openapi/runtime" ) var testProducers = map[string]runtime.Producer{ runtime.JSONMime: runtime.JSONProducer(), runtime.XMLMime: runtime.XMLProducer(), runtime.TextMime: runtime.TextProducer(), } // Test-only constants shared across the package's *_test.go files, const ( valBirdWatching = "Bird watching" valJohn = "John" valOregonTrail = "Oregon Trail" valTom = "Tom" testFile1 = "request.go" testFile2 = "request_test.go" defaultTimeout = 10 * time.Second ) func TestBuildRequest_SetHeaders(t *testing.T) { r := New(http.MethodGet, "/flats/{id}/", nil) _ = r.SetTimeout(defaultTimeout) // single value _ = r.SetHeaderParam("X-Rate-Limit", "500") assert.EqualT(t, "500", r.header.Get("X-Rate-Limit")) _ = r.SetHeaderParam("X-Rate-Limit", "400") assert.EqualT(t, "400", r.header.Get("X-Rate-Limit")) // multi value _ = r.SetHeaderParam("X-Accepts", "json", "xml", "yaml") assert.Equal(t, []string{"json", "xml", "yaml"}, r.header["X-Accepts"]) } func TestBuildRequest_SetPath(t *testing.T) { r := New(http.MethodGet, "/flats/{id}/?hello=world", nil) _ = r.SetPathParam("id", "1345") assert.EqualT(t, "1345", r.pathParams["id"]) } func TestBuildRequest_SetQuery(t *testing.T) { r := New(http.MethodGet, "/flats/{id}/", nil) // single value _ = r.SetQueryParam("hello", "there") assert.EqualT(t, "there", r.query.Get("hello")) // multi value _ = r.SetQueryParam("goodbye", "cruel", "world") assert.Equal(t, []string{"cruel", "world"}, r.query["goodbye"]) } func TestBuildRequest_SetForm(t *testing.T) { // non-multipart r := New(http.MethodPost, "/flats", nil) _ = r.SetFormParam("hello", "world") assert.EqualT(t, "world", r.formFields.Get("hello")) _ = r.SetFormParam("goodbye", "cruel", "world") assert.Equal(t, []string{"cruel", "world"}, r.formFields["goodbye"]) } func TestBuildRequest_SetFile(t *testing.T) { // needs to convert form to multipart r := New(http.MethodPost, "/flats/{id}/image", nil) // error if it isn't there err := r.SetFileParam("not there", os.NewFile(0, "./i-dont-exist")) require.Error(t, err) // error if it isn't a file err = r.SetFileParam("directory", os.NewFile(0, filepath.Join("..", "request"))) require.Error(t, err) // success adds it to the map err = r.SetFileParam("file", mustGetFile(testFile1)) require.NoError(t, err) fl, ok := r.fileFields["file"] require.TrueT(t, ok) assert.EqualT(t, testFile1, filepath.Base(fl[0].Name())) // success adds a file param with multiple files err = r.SetFileParam("otherfiles", mustGetFile(testFile1), mustGetFile(testFile2)) require.NoError(t, err) fl, ok = r.fileFields["otherfiles"] require.TrueT(t, ok) assert.EqualT(t, testFile1, filepath.Base(fl[0].Name())) assert.EqualT(t, testFile2, filepath.Base(fl[1].Name())) } func mustGetFile(pth string) *os.File { f, err := os.Open(filepath.Join(".", pth)) if err != nil { panic(err) } return f } func TestBuildRequest_SetBody(t *testing.T) { r := New(http.MethodGet, "/flats/{id}/?hello=world", nil) bd := []struct{ Name, Hobby string }{{valTom, valOregonTrail}, {valJohn, valBirdWatching}} _ = r.SetBodyParam(bd) assert.Equal(t, bd, r.payload) } func TestBuildRequest_BuildHTTPContext_PropagatesParentContext(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetTimeout(0) // disable per-request timeout: verify parent ctx alone flows through return nil }) r := New(http.MethodGet, "/", reqWrtr) type ctxKey struct{} parent := context.WithValue(t.Context(), ctxKey{}, "marker") req, cancel, err := r.BuildHTTPContext(parent, runtime.JSONMime, "", testProducers, nil, nil) require.NoError(t, err) require.NotNil(t, cancel) t.Cleanup(cancel) assert.EqualT(t, "marker", req.Context().Value(ctxKey{})) _, hasDeadline := req.Context().Deadline() assert.FalseT(t, hasDeadline, "no per-request timeout, no parent deadline -> request ctx must have no deadline") } func TestBuildRequest_BuildHTTPContext_CancelPropagates(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetTimeout(0) return nil }) r := New(http.MethodGet, "/", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "", testProducers, nil, nil) require.NoError(t, err) require.NoError(t, req.Context().Err()) cancel() require.ErrorIs(t, req.Context().Err(), context.Canceled) } func TestBuildRequest_BuildHTTPContext_AppliesPerRequestTimeout(t *testing.T) { const writerTimeout = 250 * time.Millisecond reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { // ClientRequestWriter override fires inside BuildHTTP; the derived // ctx must observe this final value, not the runtime default. return req.SetTimeout(writerTimeout) }) r := New(http.MethodGet, "/", reqWrtr) before := time.Now() req, cancel, err := r.BuildHTTPContext(context.Background(), runtime.JSONMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) deadline, ok := req.Context().Deadline() require.TrueT(t, ok, "expected request ctx to carry a deadline from the per-request timeout") delta := deadline.Sub(before) // Loose bounds — we just want to confirm it's the writerTimeout-derived deadline, // not the 30s DefaultTimeout that prepareRequest seeded. assert.TrueT(t, delta >= writerTimeout && delta < writerTimeout+time.Second, "deadline should be ~%v from now, got %v", writerTimeout, delta) } func TestBuildRequest_BuildHTTP_NoPayload(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(nil) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodPost, "/flats/{id}/", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get(strings.ToLower("X-Rate-Limit"))) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) } func TestBuildRequest_BuildHTTP_Payload(t *testing.T) { bd := []struct{ Name, Hobby string }{{valTom, valOregonTrail}, {valJohn, valBirdWatching}} reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(bd) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.JSONMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get(strings.ToLower("X-Rate-Limit"))) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) expectedBody, err := json.Marshal(bd) require.NoError(t, err) actualBody, err := io.ReadAll(req.Body) require.NoError(t, err) assert.Equal(t, append(expectedBody, '\n'), actualBody) } func TestBuildRequest_BuildHTTP_SetsInAuth(t *testing.T) { bd := []struct{ Name, Hobby string }{{valTom, valOregonTrail}, {valJohn, valBirdWatching}} reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(bd) _ = req.SetQueryParam("hello", "wrong") _ = req.SetPathParam("id", "wrong") _ = req.SetHeaderParam("X-Rate-Limit", "wrong") return nil }) auth := runtime.ClientAuthInfoWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(bd) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.JSONMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "", testProducers, nil, auth) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) expectedBody, err := json.Marshal(bd) require.NoError(t, err) actualBody, err := io.ReadAll(req.Body) require.NoError(t, err) assert.Equal(t, append(expectedBody, '\n'), actualBody) } func TestBuildRequest_BuildHTTP_XMLPayload(t *testing.T) { bd := []struct { XMLName xml.Name `xml:"person"` Name string `xml:"name"` Hobby string `xml:"hobby"` }{{xml.Name{}, valTom, valOregonTrail}, {xml.Name{}, valJohn, valBirdWatching}} reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(bd) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.XMLMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.XMLMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) expectedBody, err := xml.Marshal(bd) require.NoError(t, err) actualBody, err := io.ReadAll(req.Body) require.NoError(t, err) assert.Equal(t, expectedBody, actualBody) } func TestBuildRequest_BuildHTTP_TextPayload(t *testing.T) { const bd = "Tom: Oregon trail; John: Bird watching" reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(bd) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.TextMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.TextMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) expectedBody := []byte(bd) actualBody, err := io.ReadAll(req.Body) require.NoError(t, err) assert.Equal(t, expectedBody, actualBody) } func TestBuildRequest_BuildHTTP_Form(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetFormParam("something", "some value") _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.JSONMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) expected := []byte("something=some+value") actual, _ := io.ReadAll(req.Body) assert.Equal(t, expected, actual) } func TestBuildRequest_BuildHTTP_Form_URLEncoded(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetFormParam("something", "some value") _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.URLencodedFormMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.URLencodedFormMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, runtime.URLencodedFormMime, req.Header.Get(runtime.HeaderContentType)) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) expected := []byte("something=some+value") actual, _ := io.ReadAll(req.Body) assert.Equal(t, expected, actual) } func TestBuildRequest_BuildHTTP_Form_Content_Length(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetFormParam("something", "some value") _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.MultipartFormMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) assert.Condition(t, func() bool { return req.ContentLength > 0 }, "ContentLength must great than 0. got %d", req.ContentLength) expected := []byte("something=some+value") actual, _ := io.ReadAll(req.Body) assert.Equal(t, expected, actual) } func TestBuildRequest_BuildHTTP_FormMultipart(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetFormParam("something", "some value") _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.MultipartFormMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.MultipartFormMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) expected1 := []byte("Content-Disposition: form-data; name=\"something\"") expected2 := []byte("some value") actual, err := io.ReadAll(req.Body) require.NoError(t, err) actuallines := bytes.Split(actual, []byte("\r\n")) assert.Len(t, actuallines, 6) boundary := string(actuallines[0]) lastboundary := string(actuallines[4]) assert.TrueT(t, strings.HasPrefix(boundary, "--")) assert.TrueT(t, strings.HasPrefix(lastboundary, "--") && strings.HasSuffix(lastboundary, "--")) assert.EqualT(t, lastboundary, boundary+"--") assert.Equal(t, expected1, actuallines[1]) assert.Equal(t, expected2, actuallines[3]) } func TestBuildRequest_BuildHTTP_FormMultiples(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetFormParam("something", "some value", "another value") _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.MultipartFormMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.MultipartFormMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) expected1 := []byte("Content-Disposition: form-data; name=\"something\"") expected2 := []byte("some value") expected3 := []byte("another value") actual, err := io.ReadAll(req.Body) require.NoError(t, err) actuallines := bytes.Split(actual, []byte("\r\n")) assert.Len(t, actuallines, 10) boundary := string(actuallines[0]) lastboundary := string(actuallines[8]) assert.TrueT(t, strings.HasPrefix(boundary, "--")) assert.TrueT(t, strings.HasPrefix(lastboundary, "--") && strings.HasSuffix(lastboundary, "--")) assert.EqualT(t, lastboundary, boundary+"--") assert.Equal(t, expected1, actuallines[1]) assert.Equal(t, expected2, actuallines[3]) assert.Equal(t, actuallines[0], actuallines[4]) assert.Equal(t, expected1, actuallines[5]) assert.Equal(t, expected3, actuallines[7]) } func TestBuildRequest_BuildHTTP_Files(t *testing.T) { tmpDir := t.TempDir() cont, err := os.ReadFile(testFile1) require.NoError(t, err) cont2, err := os.ReadFile(testFile2) require.NoError(t, err) emptyFile, err := os.CreateTemp(tmpDir, "empty") require.NoError(t, err) reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetFormParam("something", "some value") _ = req.SetFileParam("file", mustGetFile(testFile1)) _ = req.SetFileParam("otherfiles", mustGetFile(testFile1), mustGetFile(testFile2)) _ = req.SetFileParam("empty", emptyFile) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.JSONMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) mediaType, params, err := mime.ParseMediaType(req.Header.Get(runtime.HeaderContentType)) require.NoError(t, err) assert.EqualT(t, runtime.MultipartFormMime, mediaType) boundary := params["boundary"] mr := multipart.NewReader(req.Body, boundary) defer req.Body.Close() frm, err := mr.ReadForm(1 << 20) require.NoError(t, err) assert.EqualT(t, "some value", frm.Value["something"][0]) fileverifier := func(name string, index int, filename string, content []byte) { mpff := frm.File[name][index] mpf, e := mpff.Open() require.NoError(t, e) defer mpf.Close() assert.EqualT(t, filename, mpff.Filename) actual, e := io.ReadAll(mpf) require.NoError(t, e) assert.Equal(t, content, actual) } fileverifier("file", 0, testFile1, cont) fileverifier("otherfiles", 0, testFile1, cont) fileverifier("otherfiles", 1, testFile2, cont2) fileverifier("empty", 0, filepath.Base(emptyFile.Name()), []byte{}) } // TestBuildRequest_BuildHTTP_Files_URLEncoded covers issue #286: when the // caller explicitly picks application/x-www-form-urlencoded, file fields must // be encoded as regular URL-encoded form values rather than producing a // multipart body advertised under a urlencoded Content-Type. func TestBuildRequest_BuildHTTP_Files_URLEncoded(t *testing.T) { cont, err := os.ReadFile(testFile2) require.NoError(t, err) cont2, err := os.ReadFile(testFile1) require.NoError(t, err) reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetFormParam("something", "some value") _ = req.SetFileParam("file", mustGetFile(testFile2)) _ = req.SetFileParam("otherfiles", mustGetFile(testFile2), mustGetFile(testFile1)) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.URLencodedFormMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.URLencodedFormMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/flats/1234/", req.URL.Path) // Content-Type must be the bare urlencoded type — no boundary parameter. assert.EqualT(t, runtime.URLencodedFormMime, req.Header.Get(runtime.HeaderContentType)) body, err := io.ReadAll(req.Body) require.NoError(t, err) defer req.Body.Close() values, err := url.ParseQuery(string(body)) require.NoError(t, err) assert.EqualT(t, "some value", values.Get("something")) require.Len(t, values["file"], 1) assert.Equal(t, string(cont), values["file"][0]) require.Len(t, values["otherfiles"], 2) assert.Equal(t, string(cont), values["otherfiles"][0]) assert.Equal(t, string(cont2), values["otherfiles"][1]) } type contentTypeProvider struct { runtime.NamedReadCloser contentType string } func (p contentTypeProvider) ContentType() string { return p.contentType } func TestBuildRequest_BuildHTTP_File_ContentType(t *testing.T) { cont, err := os.ReadFile(testFile1) require.NoError(t, err) cont2, err := os.ReadFile(testFile2) require.NoError(t, err) reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetPathParam("id", "1234") _ = req.SetFileParam("file1", contentTypeProvider{ NamedReadCloser: mustGetFile(testFile1), contentType: runtime.DefaultMime, }) _ = req.SetFileParam("file2", mustGetFile(testFile2)) return nil }) r := New(http.MethodGet, "/flats/{id}/", reqWrtr) _ = r.SetHeaderParam(runtime.HeaderContentType, runtime.JSONMime) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "/flats/1234/", req.URL.Path) mediaType, params, err := mime.ParseMediaType(req.Header.Get(runtime.HeaderContentType)) require.NoError(t, err) assert.EqualT(t, runtime.MultipartFormMime, mediaType) boundary := params["boundary"] mr := multipart.NewReader(req.Body, boundary) defer req.Body.Close() frm, err := mr.ReadForm(1 << 20) require.NoError(t, err) fileverifier := func(name string, index int, filename string, content []byte, contentType string) { mpff := frm.File[name][index] mpf, e := mpff.Open() require.NoError(t, e) defer mpf.Close() assert.EqualT(t, filename, mpff.Filename) actual, e := io.ReadAll(mpf) require.NoError(t, e) assert.Equal(t, content, actual) assert.EqualT(t, mpff.Header.Get("Content-Type"), contentType) } fileverifier("file1", 0, testFile1, cont, runtime.DefaultMime) fileverifier("file2", 0, testFile2, cont2, "text/plain; charset=utf-8") } func TestBuildRequest_BuildHTTP_BasePath(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(nil) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodPost, "/flats/{id}/", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "/basepath", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/basepath/flats/1234/", req.URL.Path) } func TestBuildRequest_BuildHTTP_EscapedPath(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(nil) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234/?*&^%") _ = req.SetHeaderParam("X-Rate-Limit", "200") return nil }) r := New(http.MethodPost, "/flats/{id}/", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "/basepath", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "200", req.Header.Get("X-Rate-Limit")) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/basepath/flats/1234/?*&^%/", req.URL.Path) assert.EqualT(t, "/basepath/flats/1234%2F%3F%2A&%5E%25/", req.URL.RawPath) assert.EqualT(t, req.URL.RawPath, req.URL.EscapedPath()) } // TestBuildRequest_BuildHTTP_RootPathTrailingSlash locks in the fix for // issue #101: the bare-root pattern "/" under a non-empty basePath must // keep its trailing slash, and the bare-root cases that the pre-fix // formula avoided ("" / "/" basePath) must still not produce "//". func TestBuildRequest_BuildHTTP_RootPathTrailingSlash(t *testing.T) { const bp = "/basepath" cases := []struct { name string basePath string pathPattern string wantPath string }{ {"root pattern under non-empty basePath keeps slash (#101)", bp, "/", bp + "/"}, {"root pattern under '/' basePath stays '/'", "/", "/", "/"}, {"root pattern under empty basePath stays '/'", "", "/", "/"}, {"non-root trailing slash still preserved", bp, "/users/", bp + "/users/"}, {"no trailing slash on pattern produces no trailing slash", bp, "/users", bp + "/users"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) r := New(http.MethodGet, tc.pathPattern, reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, tc.basePath, testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, tc.wantPath, req.URL.Path) }) } } func TestBuildRequest_BuildHTTP_BasePathWithQueryParameters(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(nil) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") return nil }) r := New(http.MethodPost, "/flats/{id}/", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "/basepath?foo=bar", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "bar", req.URL.Query().Get("foo")) assert.EqualT(t, "/basepath/flats/1234/", req.URL.Path) } func TestBuildRequest_BuildHTTP_PathPatternWithQueryParameters(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(nil) _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") return nil }) r := New(http.MethodPost, "/flats/{id}/?foo=bar", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "/basepath", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "bar", req.URL.Query().Get("foo")) assert.EqualT(t, "/basepath/flats/1234/", req.URL.Path) } func TestBuildRequest_BuildHTTP_StaticParametersPathPatternPrevails(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(nil) _ = req.SetPathParam("id", "1234") return nil }) r := New(http.MethodPost, "/flats/{id}/?hello=world", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "/basepath?hello=kitty", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "world", req.URL.Query().Get("hello")) assert.EqualT(t, "/basepath/flats/1234/", req.URL.Path) } func TestBuildRequest_BuildHTTP_StaticParametersConflictClientPrevails(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetBodyParam(nil) _ = req.SetQueryParam("hello", "there") _ = req.SetPathParam("id", "1234") return nil }) r := New(http.MethodPost, "/flats/{id}/?hello=world", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "/basepath?hello=kitty", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) assert.EqualT(t, "there", req.URL.Query().Get("hello")) assert.EqualT(t, "/basepath/flats/1234/", req.URL.Path) } // TestBuildRequest_BuildHTTP_ParametrizedFormMimes verifies that // isMultipart correctly handles legal RFC 7231 parameters and case // variants on the form mime types — i.e. it strips `; boundary=…`, // `; charset=…`, etc. before comparing against the canonical constants. // // Without the fix, three things go wrong: // - `multipart/form-data; boundary=xyz` with no files routes to the // buffered/urlencoded flow, producing a urlencoded body with a // multipart Content-Type header. // - `application/x-www-form-urlencoded; charset=utf-8` with file // fields silently switches to multipart, undoing the urlencoded // short-circuit that #286 added. // - Mixed-case `Multipart/Form-Data` misses the multipart compare // (it's exact, not case-insensitive). func TestBuildRequest_BuildHTTP_ParametrizedFormMimes(t *testing.T) { t.Run("multipart with boundary param, no files", func(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetFormParam("name", "fido") }) r := New(http.MethodPost, "/", reqWrtr) // A caller-supplied boundary that the runtime would normally // add itself — the dispatch must still recognize the base mime // as multipart and route to the streaming flow. mt := runtime.MultipartFormMime + "; boundary=caller-supplied" req, cancel, err := r.BuildHTTPContext(t.Context(), mt, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) // Streaming flow taken: req.Body is a pipe reader, and the // runtime emitted its own boundary (mangleContentType // overwrites the caller's parameter). ct := req.Header.Get(runtime.HeaderContentType) assert.TrueT(t, strings.HasPrefix(ct, runtime.MultipartFormMime+"; boundary="), "expected multipart Content-Type with runtime boundary, got %q", ct) body, err := io.ReadAll(req.Body) require.NoError(t, err) assert.Contains(t, string(body), `name="name"`) assert.Contains(t, string(body), "fido") }) t.Run("urlencoded with charset param, with files", func(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetFileParam("upload", runtime.NamedReader("doc.txt", strings.NewReader("abc"))) }) r := New(http.MethodPost, "/", reqWrtr) // Per #286, urlencoded-with-files is honored: the file content // travels inline as a regular form value. The charset param // must not break the short-circuit. mt := runtime.URLencodedFormMime + "; charset=utf-8" req, cancel, err := r.BuildHTTPContext(t.Context(), mt, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) // Buffered/urlencoded flow taken: CT is set verbatim to the // caller's mediaType (params preserved); body is the inlined // form value. assert.EqualT(t, mt, req.Header.Get(runtime.HeaderContentType)) body, err := io.ReadAll(req.Body) require.NoError(t, err) assert.EqualT(t, "upload=abc", string(body)) }) t.Run("multipart in mixed case", func(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetFormParam("name", "fido") }) r := New(http.MethodPost, "/", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), "Multipart/Form-Data", "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) // Case-insensitive recognition: streaming flow taken. ct := req.Header.Get(runtime.HeaderContentType) assert.TrueT(t, strings.HasPrefix(ct, runtime.MultipartFormMime+"; boundary="), "expected multipart Content-Type with boundary, got %q", ct) }) } // TestBuildRequest_BuildHTTP_EmptyForm verifies that a request with // neither form fields, file fields, nor payload routes through the // buffered flow regardless of mediaType — and produces a well-formed // no-body request. In particular, the multipart mime case must NOT // engage the streaming flow when there is nothing to stream (which // would spawn an idle goroutine and produce a pipe-backed body). func TestBuildRequest_BuildHTTP_EmptyForm(t *testing.T) { cases := []struct { name string mediaType string }{ {"empty + multipart mime", runtime.MultipartFormMime}, {"empty + urlencoded mime", runtime.URLencodedFormMime}, {"empty + json mime", runtime.JSONMime}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { _ = req.SetQueryParam("hello", "world") _ = req.SetPathParam("id", "1234") return nil }) r := New(http.MethodPost, "/flats/{id}/", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), tc.mediaType, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) require.NotNil(t, req) // No body source: confirms the streaming flow was not taken // (otherwise req.Body would be a pipe reader). assert.Nil(t, req.Body) // No Content-Type: there is no body to describe and the // trailing fallback only fires when body != nil. assert.Empty(t, req.Header.Get(runtime.HeaderContentType)) // URL/query wiring still applies. assert.EqualT(t, "/flats/1234/", req.URL.Path) assert.EqualT(t, "world", req.URL.Query().Get("hello")) }) } } // observableFile is a NamedReadCloser that signals on Close and never // blocks on Read. Used to verify that error paths in buildHTTP close // the body source — and, for multipart, that the spawned writer // goroutine terminates and runs its deferred file-close loop. type observableFile struct { name string data *bytes.Reader closed chan struct{} } func newObservableFile(name string, data []byte) *observableFile { return &observableFile{ name: name, data: bytes.NewReader(data), closed: make(chan struct{}), } } func (f *observableFile) Read(p []byte) (int, error) { return f.data.Read(p) } func (f *observableFile) Name() string { return f.name } func (f *observableFile) Close() error { select { case <-f.closed: // already closed default: close(f.closed) } return nil } // TestBuildRequest_BuildHTTP_MultipartGoroutineCleanupOnAuthError is a // regression test for a goroutine leak: when auth fails after // writeMultipartBody has spawned the pipe-writer goroutine, the // goroutine would park forever on pw.Write because no consumer ever // reads the pipe. The fix in buildStreamingRequest closes the pipe // reader on error paths, which unblocks the writer goroutine and lets // it run its deferred file-close. func TestBuildRequest_BuildHTTP_MultipartGoroutineCleanupOnAuthError(t *testing.T) { file := newObservableFile("data.bin", bytes.Repeat([]byte("x"), 4096)) reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetFileParam("upload", file) }) authErr := errors.New("auth failed") auth := runtime.ClientAuthInfoWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return authErr }) r := New(http.MethodPost, "/upload", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.MultipartFormMime, "", testProducers, nil, auth) require.ErrorIs(t, err, authErr) t.Cleanup(cancel) require.Nil(t, req) // The multipart goroutine must terminate (its deferred file-close // runs and signals on f.closed). Without the fix this select would // hit the timeout because the goroutine is parked on pw.Write. select { case <-file.closed: case <-time.After(2 * time.Second): t.Fatal("multipart goroutine leaked: file was never closed after auth error") } } // observableReadCloser is a stream payload whose Close is observable. type observableReadCloser struct { data *bytes.Reader closed chan struct{} } func newObservableReadCloser(data []byte) *observableReadCloser { return &observableReadCloser{data: bytes.NewReader(data), closed: make(chan struct{})} } func (r *observableReadCloser) Read(p []byte) (int, error) { return r.data.Read(p) } func (r *observableReadCloser) Close() error { select { case <-r.closed: default: close(r.closed) } return nil } // TestBuildRequest_BuildHTTP_StreamPayloadClosedOnAuthError verifies // that a stream payload's io.ReadCloser is closed when auth fails — // otherwise the user-provided closer leaks. func TestBuildRequest_BuildHTTP_StreamPayloadClosedOnAuthError(t *testing.T) { payload := newObservableReadCloser([]byte("hello")) reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetBodyParam(payload) }) authErr := errors.New("auth failed") auth := runtime.ClientAuthInfoWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return authErr }) r := New(http.MethodPost, "/stream", reqWrtr) req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, "", testProducers, nil, auth) require.ErrorIs(t, err, authErr) t.Cleanup(cancel) require.Nil(t, req) select { case <-payload.closed: case <-time.After(2 * time.Second): t.Fatal("stream payload leaked: ReadCloser was never closed after auth error") } } // TestBuildRequest_BuildHTTPContext_MultipartCancelAbortsUpload verifies that // canceling the parent context aborts an in-flight multipart upload: the // pipe consumer surfaces context.Canceled and the spawned writer goroutine // terminates, running its deferred file-close. // // Without ctx wiring inside streamMultipartParts, cancellation would only // take effect once the http transport noticed and closed the body reader, // which is too late for an upload that has not yet been handed to a // transport (e.g. test code reading req.Body directly). func TestBuildRequest_BuildHTTPContext_MultipartCancelAbortsUpload(t *testing.T) { // 4 MiB ensures io.Copy iterates over multiple buffer-sized Reads, // so the ctxReader gets several chances to observe cancellation. file := newObservableFile("big.bin", bytes.Repeat([]byte("x"), 4<<20)) reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetFileParam("upload", file) }) r := New(http.MethodPost, "/upload", reqWrtr) parentCtx, parentCancel := context.WithCancel(context.Background()) req, cancel, err := r.BuildHTTPContext(parentCtx, runtime.MultipartFormMime, "", testProducers, nil, nil) require.NoError(t, err) t.Cleanup(cancel) // Cancel before draining the body. The streaming goroutine may already // have parked on a pw.Write inside the part header; once we begin // reading, the next ctxReader.Read sees the canceled ctx and the pipe // is closed with context.Canceled. parentCancel() _, err = io.ReadAll(req.Body) require.ErrorIs(t, err, context.Canceled) select { case <-file.closed: case <-time.After(2 * time.Second): t.Fatal("multipart goroutine did not close file after cancellation") } } go-openapi-runtime-decad8f/client/keepalive.go000066400000000000000000000027121520232310000216310ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "io" "net/http" "sync/atomic" ) // KeepAliveTransport drains the remaining body from a response // so that go will reuse the TCP connections. // This is not enabled by default because there are servers where // the response never gets closed and that would make the code hang forever. // So instead it's provided as a [http] client [middleware] that can be used to override // any request. func KeepAliveTransport(rt http.RoundTripper) http.RoundTripper { return &keepAliveTransport{wrapped: rt} } type keepAliveTransport struct { wrapped http.RoundTripper } func (k *keepAliveTransport) RoundTrip(r *http.Request) (*http.Response, error) { resp, err := k.wrapped.RoundTrip(r) if err != nil { return resp, err } resp.Body = &drainingReadCloser{rdr: resp.Body} return resp, nil } type drainingReadCloser struct { rdr io.ReadCloser seenEOF atomic.Uint32 } func (d *drainingReadCloser) Read(p []byte) (n int, err error) { n, err = d.rdr.Read(p) if err == io.EOF || n == 0 { d.seenEOF.Store(1) } return } func (d *drainingReadCloser) Close() error { // drain buffer if d.seenEOF.Load() != 1 { // If the reader side (a HTTP server) is misbehaving, it still may send // some bytes, but the closer ignores them to keep the underling // connection open. _, _ = io.Copy(io.Discard, d.rdr) } return d.rdr.Close() } go-openapi-runtime-decad8f/client/keepalive_test.go000066400000000000000000000035731520232310000226760ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "bytes" "io" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func newCountingReader(rdr io.Reader, readOnce bool) *countingReadCloser { return &countingReadCloser{ rdr: rdr, readOnce: readOnce, } } type countingReadCloser struct { rdr io.Reader readOnce bool readCalled int closeCalled int } func (c *countingReadCloser) Read(b []byte) (int, error) { c.readCalled++ if c.readCalled > 1 && c.readOnce { return 0, io.EOF } return c.rdr.Read(b) } func (c *countingReadCloser) Close() error { c.closeCalled++ return nil } func TestDrainingReadCloser(t *testing.T) { rdr := newCountingReader(bytes.NewBufferString("There are many things to do"), false) prevDisc := io.Discard disc := bytes.NewBuffer(nil) io.Discard = disc defer func() { io.Discard = prevDisc }() buf := make([]byte, 5) ts := &drainingReadCloser{rdr: rdr} _, err := ts.Read(buf) require.NoError(t, err) require.NoError(t, ts.Close()) assert.EqualT(t, "There", string(buf)) assert.EqualT(t, " are many things to do", disc.String()) assert.EqualT(t, 3, rdr.readCalled) assert.EqualT(t, 1, rdr.closeCalled) } func TestDrainingReadCloser_SeenEOF(t *testing.T) { rdr := newCountingReader(bytes.NewBufferString("There are many things to do"), true) prevDisc := io.Discard disc := bytes.NewBuffer(nil) io.Discard = disc defer func() { io.Discard = prevDisc }() buf := make([]byte, 5) ts := &drainingReadCloser{rdr: rdr} _, err := ts.Read(buf) require.NoError(t, err) _, err = ts.Read(nil) require.ErrorIs(t, err, io.EOF) require.NoError(t, ts.Close()) assert.EqualT(t, "There", string(buf)) assert.Empty(t, disc.String()) assert.EqualT(t, 2, rdr.readCalled) assert.EqualT(t, 1, rdr.closeCalled) } go-openapi-runtime-decad8f/client/mock_test.go000066400000000000000000000030241520232310000216510ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "bytes" "context" "io" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" ) type mockRuntime struct { req runtime.TestClientRequest } func (m *mockRuntime) Submit(operation *runtime.ClientOperation) (any, error) { _ = operation.Params.WriteToRequest(&m.req, nil) _, _ = operation.Reader.ReadResponse(&tres{}, nil) return map[string]any{}, nil } type tres struct { } func (r tres) Code() int { return 490 } func (r tres) Message() string { return "the message" } func (r tres) GetHeader(_ string) string { return "the header" } func (r tres) GetHeaders(_ string) []string { return []string{"the headers", "the headers2"} } func (r tres) Body() io.ReadCloser { return io.NopCloser(bytes.NewBufferString("the content")) } func testOperation(ctx context.Context) *runtime.ClientOperation { return &runtime.ClientOperation{ ID: "getCluster", Method: "GET", PathPattern: "/kubernetes-clusters/{cluster_id}", ProducesMediaTypes: []string{"application/json"}, ConsumesMediaTypes: []string{"application/json"}, Schemes: []string{"https"}, Reader: runtime.ClientResponseReaderFunc(func(runtime.ClientResponse, runtime.Consumer) (any, error) { return nil, nil }), Params: runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }), AuthInfo: PassThroughAuth, Context: ctx, } } go-openapi-runtime-decad8f/client/opentelemetry.go000066400000000000000000000170441520232310000225640ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "fmt" "net/http" "strings" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.37.0" "go.opentelemetry.io/otel/trace" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" ) const ( instrumentationVersion = "1.0.0" tracerName = "go-openapi" ) // WithOpenTelemetry adds opentelemetry support to the provided runtime. // A new client span is created for each request. // If the context of the client operation does not contain an active span, no span is created. // The provided opts are applied to each spans - for example to add global tags. func (r *Runtime) WithOpenTelemetry(opts ...OpenTelemetryOpt) runtime.ClientTransport { return newOpenTelemetryTransport(r, r.Host, opts) } // WithOpenTracing adds opentracing support to the provided runtime. // A new client span is created for each request. // If the context of the client operation does not contain an active span, no span is created. // The provided opts are applied to each spans - for example to add global tags. // // Deprecated: use [WithOpenTelemetry] instead, as opentracing is now archived and superseded by opentelemetry. // // # Deprecation notice // // The [Runtime.WithOpenTracing] method has been deprecated in favor of [Runtime.WithOpenTelemetry]. // // The method is still around so programs calling it will still build. However, it will return // an opentelemetry transport. // // If you have a strict requirement on using opentracing, you may still do so by importing // module [github.com/go-openapi/runtime/client-[middleware]/opentracing] and using // [github.com/go-openapi/runtime/client-[middleware]/opentracing.WithOpenTracing] with your // usual opentracing options and opentracing-enabled transport. // // Passed options are ignored unless they are of type [OpenTelemetryOpt]. func (r *Runtime) WithOpenTracing(opts ...any) runtime.ClientTransport { otelOpts := make([]OpenTelemetryOpt, 0, len(opts)) for _, o := range opts { otelOpt, ok := o.(OpenTelemetryOpt) if !ok { continue } otelOpts = append(otelOpts, otelOpt) } return r.WithOpenTelemetry(otelOpts...) } type config struct { Tracer trace.Tracer Propagator propagation.TextMapPropagator SpanStartOptions []trace.SpanStartOption SpanNameFormatter func(*runtime.ClientOperation) string TracerProvider trace.TracerProvider } type OpenTelemetryOpt interface { apply(*config) } type optionFunc func(*config) func (o optionFunc) apply(c *config) { o(c) } // WithTracerProvider specifies a tracer provider to use for creating a tracer. // If none is specified, the global provider is used. func WithTracerProvider(provider trace.TracerProvider) OpenTelemetryOpt { return optionFunc(func(c *config) { if provider != nil { c.TracerProvider = provider } }) } // WithPropagators configures specific propagators. If this // option isn't specified, then the global TextMapPropagator is used. func WithPropagators(ps propagation.TextMapPropagator) OpenTelemetryOpt { return optionFunc(func(c *config) { if ps != nil { c.Propagator = ps } }) } // WithSpanOptions configures an additional set of // trace.SpanOptions, which are applied to each new span. func WithSpanOptions(opts ...trace.SpanStartOption) OpenTelemetryOpt { return optionFunc(func(c *config) { c.SpanStartOptions = append(c.SpanStartOptions, opts...) }) } // WithSpanNameFormatter takes a function that will be called on every // request and the returned string will become the Span Name. func WithSpanNameFormatter(f func(op *runtime.ClientOperation) string) OpenTelemetryOpt { return optionFunc(func(c *config) { c.SpanNameFormatter = f }) } func defaultTransportFormatter(op *runtime.ClientOperation) string { if op.ID != "" { return op.ID } return fmt.Sprintf("%s_%s", strings.ToLower(op.Method), op.PathPattern) } type openTelemetryTransport struct { transport runtime.ClientTransport host string tracer trace.Tracer config *config } func newOpenTelemetryTransport(transport runtime.ClientTransport, host string, opts []OpenTelemetryOpt) *openTelemetryTransport { tr := &openTelemetryTransport{ transport: transport, host: host, } const baseOptions = 4 defaultOpts := make([]OpenTelemetryOpt, 0, len(opts)+baseOptions) defaultOpts = append(defaultOpts, WithSpanOptions(trace.WithSpanKind(trace.SpanKindClient)), WithSpanNameFormatter(defaultTransportFormatter), WithPropagators(otel.GetTextMapPropagator()), WithTracerProvider(otel.GetTracerProvider()), ) c := newConfig(append(defaultOpts, opts...)...) tr.config = c return tr } func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (any, error) { if op.Context == nil { return t.transport.Submit(op) } params := op.Params reader := op.Reader var span trace.Span defer func() { if span != nil { span.End() } }() op.Params = runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error { span = t.newOpenTelemetrySpan(op, req.GetHeaderParams()) return params.WriteToRequest(req, reg) }) op.Reader = runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if span != nil { statusCode := response.Code() // NOTE: this is replaced by semconv.HTTPResponseStatusCode in semconv v1.21 span.SetAttributes(semconv.HTTPResponseStatusCode(statusCode)) // NOTE: the conversion from HTTP status code to trace code is no longer available with // semconv v1.21 const minHTTPStatusIsError = 400 if statusCode >= minHTTPStatusIsError { span.SetStatus(codes.Error, http.StatusText(statusCode)) } } return reader.ReadResponse(response, consumer) }) submit, err := t.transport.Submit(op) if err != nil && span != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) } return submit, err } func (t *openTelemetryTransport) newOpenTelemetrySpan(op *runtime.ClientOperation, header http.Header) trace.Span { ctx := op.Context tracer := t.tracer if tracer == nil { if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() { tracer = newTracer(span.TracerProvider()) } else { tracer = newTracer(otel.GetTracerProvider()) } } ctx, span := tracer.Start(ctx, t.config.SpanNameFormatter(op), t.config.SpanStartOptions...) var scheme string if len(op.Schemes) > 0 { scheme = op.Schemes[0] } span.SetAttributes( attribute.String("net.peer.name", t.host), attribute.String(string(semconv.HTTPRouteKey), op.PathPattern), attribute.String(string(semconv.HTTPRequestMethodKey), op.Method), attribute.String("span.kind", trace.SpanKindClient.String()), attribute.String("http.scheme", scheme), ) carrier := propagation.HeaderCarrier(header) t.config.Propagator.Inject(ctx, carrier) return span } func newTracer(tp trace.TracerProvider) trace.Tracer { return tp.Tracer(tracerName, trace.WithInstrumentationVersion(version())) } func newConfig(opts ...OpenTelemetryOpt) *config { c := &config{ Propagator: otel.GetTextMapPropagator(), } for _, opt := range opts { opt.apply(c) } // Tracer is only initialized if manually specified. Otherwise, can be passed with the tracing context. if c.TracerProvider != nil { c.Tracer = newTracer(c.TracerProvider) } return c } // Version is the current release version of the go-runtime instrumentation. func version() string { return instrumentationVersion } go-openapi-runtime-decad8f/client/opentelemetry_test.go000066400000000000000000000073321520232310000236220ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "context" "net/http" "testing" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" tracesdk "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" "go.opentelemetry.io/otel/trace" "github.com/go-openapi/runtime" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func Test_OpenTelemetryRuntime_submit(t *testing.T) { t.Parallel() exporter := tracetest.NewInMemoryExporter() tp := tracesdk.NewTracerProvider( tracesdk.WithSampler(tracesdk.AlwaysSample()), tracesdk.WithSyncer(exporter), ) otel.SetTracerProvider(tp) tracer := tp.Tracer("go-runtime") ctx, span := tracer.Start(context.Background(), "op") defer span.End() assertOpenTelemetrySubmit(t, testOperation(ctx), exporter, 1) } func Test_OpenTelemetryRuntime_submit_nilAuthInfo(t *testing.T) { t.Parallel() exporter := tracetest.NewInMemoryExporter() tp := tracesdk.NewTracerProvider( tracesdk.WithSampler(tracesdk.AlwaysSample()), tracesdk.WithSyncer(exporter), ) otel.SetTracerProvider(tp) tracer := tp.Tracer("go-runtime") ctx, span := tracer.Start(context.Background(), "op") defer span.End() operation := testOperation(ctx) operation.AuthInfo = nil assertOpenTelemetrySubmit(t, operation, exporter, 1) } func Test_OpenTelemetryRuntime_submit_nilContext(t *testing.T) { exporter := tracetest.NewInMemoryExporter() tp := tracesdk.NewTracerProvider( tracesdk.WithSampler(tracesdk.AlwaysSample()), tracesdk.WithSyncer(exporter), ) otel.SetTracerProvider(tp) tracer := tp.Tracer("go-runtime") ctx, span := tracer.Start(context.Background(), "op") defer span.End() operation := testOperation(ctx) operation.Context = nil assertOpenTelemetrySubmit(t, operation, exporter, 0) // just don't panic } func Test_injectOpenTelemetrySpanContext(t *testing.T) { t.Parallel() exporter := tracetest.NewInMemoryExporter() tp := tracesdk.NewTracerProvider( tracesdk.WithSampler(tracesdk.AlwaysSample()), tracesdk.WithSyncer(exporter), ) otel.SetTracerProvider(tp) tracer := tp.Tracer("go-runtime") ctx, span := tracer.Start(context.Background(), "op") defer span.End() operation := testOperation(ctx) header := map[string][]string{} tr := newOpenTelemetryTransport(&mockRuntime{runtime.TestClientRequest{Headers: header}}, "", nil) tr.config.Propagator = propagation.TraceContext{} _, err := tr.Submit(operation) require.NoError(t, err) assert.Len(t, header, 1) } func assertOpenTelemetrySubmit(t *testing.T, operation *runtime.ClientOperation, exporter *tracetest.InMemoryExporter, expectedSpanCount int) { header := map[string][]string{} tr := newOpenTelemetryTransport(&mockRuntime{runtime.TestClientRequest{Headers: header}}, "remote_host", nil) _, err := tr.Submit(operation) require.NoError(t, err) spans := exporter.GetSpans() assert.Len(t, spans, expectedSpanCount) if expectedSpanCount != 1 { return } span := spans[0] assert.EqualT(t, "getCluster", span.Name) assert.EqualT(t, "go-openapi", span.InstrumentationScope.Name) assert.EqualT(t, codes.Error, span.Status.Code) assert.Equal(t, []attribute.KeyValue{ attribute.String("net.peer.name", "remote_host"), attribute.String("http.route", "/kubernetes-clusters/{cluster_id}"), attribute.String("http.request.method", http.MethodGet), attribute.String("span.kind", trace.SpanKindClient.String()), attribute.String("http.scheme", schemeHTTPS), // NOTE: this becomes http.response.status_code with semconv v1.21 attribute.Int("http.response.status_code", 490), }, span.Attributes) } go-openapi-runtime-decad8f/client/response.go000066400000000000000000000013371520232310000215240ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "io" "net/http" "github.com/go-openapi/runtime" ) var _ runtime.ClientResponse = response{} func newResponse(resp *http.Response) runtime.ClientResponse { return response{resp: resp} } type response struct { resp *http.Response } func (r response) Code() int { return r.resp.StatusCode } func (r response) Message() string { return r.resp.Status } func (r response) GetHeader(name string) string { return r.resp.Header.Get(name) } func (r response) GetHeaders(name string) []string { return r.resp.Header.Values(name) } func (r response) Body() io.ReadCloser { return r.resp.Body } go-openapi-runtime-decad8f/client/response_test.go000066400000000000000000000014731520232310000225640ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "bytes" "io" "net/http" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/runtime" ) func TestResponse(t *testing.T) { under := new(http.Response) under.Status = "the status message" under.StatusCode = 392 under.Header = make(http.Header) under.Header.Set("Blah", "blahblah") under.Body = io.NopCloser(bytes.NewBufferString("some content")) var resp runtime.ClientResponse = response{under} assert.EqualT(t, under.StatusCode, resp.Code()) assert.EqualT(t, under.Status, resp.Message()) assert.EqualT(t, "blahblah", resp.GetHeader("blah")) assert.Equal(t, []string{"blahblah"}, resp.GetHeaders("blah")) assert.Equal(t, under.Body, resp.Body()) } go-openapi-runtime-decad8f/client/runtime.go000066400000000000000000000426501520232310000213540ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "context" "fmt" "mime" "net/http" "net/http/httputil" "strings" "sync" "time" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client/internal/request" "github.com/go-openapi/runtime/logger" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/server-middleware/mediatype" "github.com/go-openapi/runtime/yamlpc" "github.com/go-openapi/strfmt" ) const ( schemeHTTP = "http" schemeHTTPS = "https" ) // DefaultTimeout the default request timeout. var DefaultTimeout = 30 * time.Second // Runtime represents an API client that uses the transport // to make [http] requests based on a swagger specification. type Runtime struct { DefaultMediaType string DefaultAuthentication runtime.ClientAuthInfoWriter Consumers map[string]runtime.Consumer Producers map[string]runtime.Producer Transport http.RoundTripper Jar http.CookieJar // Spec *spec.Document Host string BasePath string Formats strfmt.Registry Context context.Context //nolint:containedctx // we precisely want this type to contain the request context Debug bool // Trace enables connection-level diagnostic output via // [net/http/httptrace]. When true, the runtime narrates the // connection lifecycle of every request through r.logger.Debugf: // DNS, dial, TLS handshake, idle-pool reuse, request body // transfer, time-to-first-byte, response body transfer, and a // trailing per-request summary line. // // Trace is orthogonal to Debug: Debug dumps wire bytes (request // and response headers and body), Trace narrates how the // connection got there. Both may be enabled independently. // // Trace is not coupled to the SWAGGER_DEBUG / DEBUG environment // variables: it defaults to false and is only enabled by // explicit assignment. // // Trace is primarily intended as a problem-investigation tool // (the local equivalent of curl -vvv), not an always-on tracer. // For distributed-trace correlation, use the OpenTelemetry // integration ([Runtime.WithOpenTelemetry]). Trace bool logger logger.Logger // MatchSuffix enables RFC 6839 structured-syntax suffix tolerance // for codec lookup. When true, a response with Content-Type // "application/problem+json" finds the JSON consumer registered // under "application/json"; with the default false, the lookup // is strict and falls through to the "*/*" wildcard if present. // See [mediatype.AllowSuffix] for the semantics. MatchSuffix bool clientOnce *sync.Once client *http.Client schemes []string response ClientResponseFunc } // New creates a new default runtime for a swagger api runtime.Client. func New(host, basePath string, schemes []string) *Runtime { var rt Runtime rt.DefaultMediaType = runtime.JSONMime // Enhancement proposal: https://github.com/go-openapi/runtime/issues/385 rt.Consumers = map[string]runtime.Consumer{ runtime.YAMLMime: yamlpc.YAMLConsumer(), runtime.JSONMime: runtime.JSONConsumer(), runtime.XMLMime: runtime.XMLConsumer(), runtime.TextMime: runtime.TextConsumer(), runtime.HTMLMime: runtime.TextConsumer(), runtime.CSVMime: runtime.CSVConsumer(), runtime.DefaultMime: runtime.ByteStreamConsumer(), } rt.Producers = map[string]runtime.Producer{ runtime.YAMLMime: yamlpc.YAMLProducer(), runtime.JSONMime: runtime.JSONProducer(), runtime.XMLMime: runtime.XMLProducer(), runtime.TextMime: runtime.TextProducer(), runtime.HTMLMime: runtime.TextProducer(), runtime.CSVMime: runtime.CSVProducer(), runtime.DefaultMime: runtime.ByteStreamProducer(), } rt.Transport = http.DefaultTransport rt.Jar = nil rt.Host = host rt.BasePath = basePath rt.Context = context.Background() rt.clientOnce = new(sync.Once) if !strings.HasPrefix(rt.BasePath, "/") { rt.BasePath = "/" + rt.BasePath } rt.Debug = logger.DebugEnabled() rt.logger = logger.StandardLogger{} rt.response = newResponse if len(schemes) > 0 { rt.schemes = schemes } return &rt } // NewWithClient allows you to create a new transport with a configured [http.Client]. func NewWithClient(host, basePath string, schemes []string, client *http.Client) *Runtime { rt := New(host, basePath, schemes) if client != nil { rt.clientOnce.Do(func() { rt.client = client }) } return rt } // EnableConnectionReuse drains the remaining body from a response // so that go will reuse the TCP connections. // // This is not enabled by default because there are servers where // the response never gets closed and that would make the code hang forever. // So instead it's provided as a [http] client [middleware] that can be used to override // any request. func (r *Runtime) EnableConnectionReuse() { if r.client == nil { r.Transport = KeepAliveTransport( transportOrDefault(r.Transport, http.DefaultTransport), ) return } r.client.Transport = KeepAliveTransport( transportOrDefault(r.client.Transport, transportOrDefault(r.Transport, http.DefaultTransport), ), ) } // CreateHTTPRequestContext creates the requests and bind the parameters, but does not send it over the wire // like [Runtime.SubmitContext]. // // The [http.Request] is complete with authentication, headers and body (including streamed body) and ready for callers // to submit it to a [http.Client] of their choice, then consume the [http.Response]. // // Most users would simply use [Runtime.SubmitContext], which wraps all these operations in one call. func (r *Runtime) CreateHTTPRequestContext(ctx context.Context, operation *runtime.ClientOperation) (req *http.Request, cancel context.CancelFunc, err error) { req, cancel, err = r.createHTTPRequestContext(ctx, operation) return } // CreateHttpRequest builds the [http.Request] for the given operation, using // [context.Background] as the request context. // // Any per-operation timeout declared by the operation's [runtime.ClientRequestWriter] // is silently ignored here, which can leak a context-cancellation channel if the // caller relies on it. // // Deprecated: use [Runtime.CreateHTTPRequestContext] instead, with explicit // control over the request context and its cancellation. func (r *Runtime) CreateHttpRequest(operation *runtime.ClientOperation) (req *http.Request, err error) { //nolint:revive req, _, err = r.createHTTPRequestContext(context.Background(), operation) return } // Submit a request and when there is a body on success it will turn that into the result // all other things are turned into an api error for swagger which retains the status code. // // This call inherits the context possibly put in the operation, otherwise the one possibly put in the [Runtime]. // If none are set, use [context.Background]. // // Any timeout set by parameters is honored. func (r *Runtime) Submit(operation *runtime.ClientOperation) (any, error) { return r.SubmitContext(r.ensureContext(operation), operation) } // SubmitContext submits a request and returns the result. // // Errors are turned into an api error for swagger which retains the status code. // // Unlike [Submit], [SubmitContext] only injects the context provided by the caller: // contexts possibly cached in operation or runtime are ignored. // // On the other hand, a timeout set by parameters is honored. func (r *Runtime) SubmitContext(parentCtx context.Context, operation *runtime.ClientOperation) (any, error) { req, cancel, err := r.createHTTPRequestContext(parentCtx, operation) if err != nil { return nil, err } defer cancel() r.ensureClient() if err := r.dumpRequest(req); err != nil { return nil, err } // Attach the trace session before Do so the httptrace hooks // fire during the round-trip. The session emits its trailing // summary on finish; the response body is consumed by // ReadResponse downstream, after which finish is called. var trace *traceSession if r.Trace { trace = newTraceSession(r.logger, req.Method, req.URL.String(), introspectTLSConfig(r.pickClient(operation))) //nolint:contextcheck // We intentionally derive from req.Context() to layer the trace hooks onto the existing request context. req = req.WithContext(trace.attach(req.Context())) if req.Body != nil { req.Body = trace.wrapRequestBody(req.Body) } defer trace.finish() } res, err := r.pickClient(operation).Do(req) if err != nil { if trace != nil { trace.onRoundTripError(err) } return nil, err } defer res.Body.Close() if trace != nil { trace.onResponse(res.StatusCode) res.Body = trace.wrapResponseBody(res.Body) } ct := res.Header.Get(runtime.HeaderContentType) if ct == "" { // this should really never occur ct = r.DefaultMediaType } if err := r.dumpResponse(res, ct); err != nil { return nil, err } cons, err := r.resolveConsumer(ct) if err != nil { return nil, err } return operation.Reader.ReadResponse(r.response(res), cons) } // SetDebug changes the debug flag. // It ensures that client and middlewares have the set debug level. func (r *Runtime) SetDebug(debug bool) { r.Debug = debug middleware.Debug = debug } // SetLogger changes the logger stream. // It ensures that client and middlewares use the same logger. func (r *Runtime) SetLogger(logger logger.Logger) { r.logger = logger middleware.Logger = logger } type ClientResponseFunc = func(*http.Response) runtime.ClientResponse //nolint:revive // SetResponseReader changes the response reader implementation. func (r *Runtime) SetResponseReader(f ClientResponseFunc) { if f == nil { return } r.response = f } func (r *Runtime) ensureContext(operation *runtime.ClientOperation) context.Context { switch { case operation.Context != nil: return operation.Context case r.Context != nil: return r.Context default: return context.Background() } } func (r *Runtime) pickScheme(schemes []string) string { if v := r.selectScheme(r.schemes); v != "" { return v } if v := r.selectScheme(schemes); v != "" { return v } return schemeHTTP } func (r *Runtime) selectScheme(schemes []string) string { schLen := len(schemes) if schLen == 0 { return "" } scheme := schemes[0] // prefer https, but skip when not possible if scheme != schemeHTTPS && schLen > 1 { for _, sch := range schemes { if sch == schemeHTTPS { scheme = sch break } } } return scheme } func transportOrDefault(left, right http.RoundTripper) http.RoundTripper { if left == nil { return right } return left } // ensureClient lazily initializes r.client from r.Transport and r.Jar // on first use. Safe under concurrent calls via sync.Once. func (r *Runtime) ensureClient() { r.clientOnce.Do(func() { r.client = &http.Client{ Transport: r.Transport, Jar: r.Jar, } }) } // pickClient returns the http.Client to use for this operation: the // per-operation override if set, else the runtime's shared client. func (r *Runtime) pickClient(operation *runtime.ClientOperation) *http.Client { if operation.Client != nil { return operation.Client } return r.client } // dumpRequest writes the outgoing request to the debug logger when // r.Debug is enabled. No-op otherwise. Returns the dump error so the // caller can decide whether to abort the submit. func (r *Runtime) dumpRequest(req *http.Request) error { if !r.Debug { return nil } b, err := httputil.DumpRequestOut(req, true) if err != nil { return err } r.logger.Debugf("%s\n", string(b)) return nil } // dumpResponse writes the incoming response to the debug logger when // r.Debug is enabled. The body is omitted for runtime.DefaultMime // (binary blob). No-op otherwise. func (r *Runtime) dumpResponse(res *http.Response, ct string) error { if !r.Debug { return nil } printBody := ct != runtime.DefaultMime // Spare the terminal from a binary blob. b, err := httputil.DumpResponse(res, printBody) if err != nil { return err } r.logger.Debugf("%s\n", string(b)) return nil } // resolveConsumer parses ct and returns the registered Consumer for // that media type. Lookup is alias-aware (RFC 9512 §2.1 — yaml // aliases) and, when [Runtime.MatchSuffix] is true, also tolerates // RFC 6839 structured-syntax suffix media types (+json, +xml, +yaml). // Falls back to the "*/*" entry if no match found. func (r *Runtime) resolveConsumer(ct string) (runtime.Consumer, error) { if _, _, err := mime.ParseMediaType(ct); err != nil { return nil, fmt.Errorf("parse content type: %w", err) } if cons, ok := mediatype.Lookup(r.Consumers, ct, r.matchOpts()...); ok { return cons, nil } if cons, ok := r.Consumers["*/*"]; ok { return cons, nil } // scream about not knowing what to do return nil, fmt.Errorf("no consumer: %q", ct) } // matchOpts builds the mediatype.MatchOption slice for codec // lookups on the Runtime, currently just the AllowSuffix opt-in. func (r *Runtime) matchOpts() []mediatype.MatchOption { if !r.MatchSuffix { return nil } return []mediatype.MatchOption{mediatype.AllowSuffix()} } // createHTTPRequestContext is the context-aware builder of a [http.Request]. // // The returned [http.Request] carries a context derived from parentCtx that // honors the per-request timeout set during WriteToRequest. Callers must // invoke cancel once the response is fully read. func (r *Runtime) createHTTPRequestContext(parentCtx context.Context, operation *runtime.ClientOperation) (*http.Request, context.CancelFunc, error) { req, cmt, auth, err := r.prepareRequest(operation) if err != nil { return nil, nil, err } httpReq, cancel, err := req.BuildHTTPContext(parentCtx, cmt, r.BasePath, r.Producers, r.Formats, auth) if err != nil { return nil, nil, err } r.applyHostScheme(httpReq, operation) return httpReq, cancel, nil } // prepareRequest performs the operation-to-request setup that is // independent of how the http.Request is finally assembled: parameters, // headers, default authentication, and consumes-media-type selection. func (r *Runtime) prepareRequest(operation *runtime.ClientOperation) (*request.Request, string, runtime.ClientAuthInfoWriter, error) { params, _, auth := operation.Params, operation.Reader, operation.AuthInfo req := request.New(operation.Method, operation.PathPattern, params) _ = req.SetTimeout(DefaultTimeout) // the timeout may be overridden by ClientRequestWriter req.SetConsumes(operation.ConsumesMediaTypes) accept := make([]string, 0, len(operation.ProducesMediaTypes)) accept = append(accept, operation.ProducesMediaTypes...) if err := req.SetHeaderParam(runtime.HeaderAccept, accept...); err != nil { return nil, "", nil, err } if auth == nil && r.DefaultAuthentication != nil { auth = runtime.ClientAuthInfoWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error { if req.GetHeaderParams().Get(runtime.HeaderAuthorization) != "" { return nil } return r.DefaultAuthentication.AuthenticateRequest(req, reg) }) } cmt := pickConsumesMediaType(operation.ConsumesMediaTypes, r.Producers, r.DefaultMediaType, r.matchOpts()...) if _, ok := mediatype.Lookup(r.Producers, cmt, r.matchOpts()...); !ok && cmt != runtime.MultipartFormMime && cmt != runtime.URLencodedFormMime { return nil, "", nil, fmt.Errorf("none of producers: %v registered. try %s", r.Producers, cmt) } return req, cmt, auth, nil } // applyHostScheme stamps the runtime's host and the operation-selected // scheme onto the freshly built http.Request. func (r *Runtime) applyHostScheme(httpReq *http.Request, operation *runtime.ClientOperation) { httpReq.URL.Scheme = r.pickScheme(operation.Schemes) httpReq.URL.Host = r.Host httpReq.Host = r.Host } // pickConsumesMediaType selects which Content-Type the client will send. // // Selection rules, in priority order: // // 1. multipart/form-data if any consumes entry advertises it (it streams // and preserves per-file Content-Type, regardless of codegen ordering; // resolves issue #286); // 2. the first non-empty entry whose mime is either structural // (multipart/form-data or application/x-www-form-urlencoded — these // do not need a producer in the map) or has a producer registered in // producers — this lets the client gracefully skip unregistered // spec entries instead of erroring at the gate that follows; // 3. the first non-empty entry overall (preserves the historical error // path: the gate at the call site reports "none of producers" with // the unregistered mime, so the diagnostic is unchanged when nothing // in consumes is registered); // 4. def, if consumes is empty or all empty strings. // // Step 2 closes part of issues #32 and #386: an operation declaring // `consumes: [application/x-vendor, application/json]` with no vendor // producer registered now silently uses JSON instead of erroring. func pickConsumesMediaType(consumes []string, producers map[string]runtime.Producer, def string, opts ...mediatype.MatchOption) string { for _, mt := range consumes { if strings.EqualFold(mt, runtime.MultipartFormMime) { return mt } } var firstNonEmpty string for _, mt := range consumes { if mt == "" { continue } if firstNonEmpty == "" { firstNonEmpty = mt } if isStructuralMime(mt) { return mt } if _, ok := mediatype.Lookup(producers, mt, opts...); ok { return mt } } if firstNonEmpty != "" { return firstNonEmpty } return def } // isStructuralMime reports whether mt is a media type whose body shape // is owned by the runtime (multipart envelope, urlencoded form). These // do not require an entry in the producers map. func isStructuralMime(mt string) bool { return strings.EqualFold(mt, runtime.MultipartFormMime) || strings.EqualFold(mt, runtime.URLencodedFormMime) } go-openapi-runtime-decad8f/client/runtime_test.go000066400000000000000000001245371520232310000224200ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "bytes" "context" "encoding/json" "encoding/xml" "errors" "fmt" "io" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" "testing" "time" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) const ( token = "the-super-secret-token" bearerToken = "Bearer " + token charsetUTF8 = ";charset=utf-8" ) // task This describes a task. Tasks require a content property to be set. type task struct { // Completed Completed bool `json:"completed" xml:"completed"` // Content Task content can contain [GFM](https://help.github.com/articles/github-flavored-markdown/). Content string `json:"content" xml:"content"` // ID This id property is autogenerated when a task is created. ID int64 `json:"id" xml:"id"` } type testCtxKey uint8 const rtKey testCtxKey = 1 func TestRuntime_Concurrent(t *testing.T) { // test that it can make a simple request // and get the response for it. // defaults all the way down result := []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, } server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime) rw.WriteHeader(http.StatusOK) jsongen := json.NewEncoder(rw) assert.NoError(t, jsongen.Encode(result)) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) resCC := make(chan any) errCC := make(chan error) var res any for range 6 { go func() { resC := make(chan any) errC := make(chan error) go func() { var resp any var errp error for range 3 { resp, errp = rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res []task if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), }) <-time.After(100 * time.Millisecond) } resC <- resp errC <- errp }() resCC <- <-resC errCC <- <-errC }() } c := 6 for c > 0 { res = <-resCC err = <-errCC c-- } require.NoError(t, err) assert.IsType(t, []task{}, res) actual, ok := res.([]task) require.TrueT(t, ok) assert.Equal(t, result, actual) } func TestRuntime_Canary(t *testing.T) { // test that it can make a simple request // and get the response for it. // defaults all the way down result := []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, } server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime) rw.WriteHeader(http.StatusOK) jsongen := json.NewEncoder(rw) assert.NoError(t, jsongen.Encode(result)) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res []task if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), }) require.NoError(t, err) assert.IsType(t, []task{}, res) actual, ok := res.([]task) require.TrueT(t, ok) assert.Equal(t, result, actual) } type tasks struct { Tasks []task `xml:"task"` } func TestRuntime_XMLCanary(t *testing.T) { // test that it can make a simple XML request // and get the response for it. result := tasks{ Tasks: []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, }, } server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Add(runtime.HeaderContentType, runtime.XMLMime) rw.WriteHeader(http.StatusOK) xmlgen := xml.NewEncoder(rw) assert.NoError(t, xmlgen.Encode(result)) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res tasks if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), }) require.NoError(t, err) assert.IsType(t, tasks{}, res) actual, ok := res.(tasks) require.TrueT(t, ok) assert.Equal(t, result, actual) } func TestRuntime_TextCanary(t *testing.T) { // test that it can make a simple text request // and get the response for it. result := "1: task 1 content; 2: task 2 content" server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Add(runtime.HeaderContentType, runtime.TextMime) rw.WriteHeader(http.StatusOK) _, _ = rw.Write([]byte(result)) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res string if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), }) require.NoError(t, err) assert.IsType(t, "", res) actual, ok := res.(string) require.TrueT(t, ok) assert.EqualT(t, result, actual) } func TestRuntime_CSVCanary(t *testing.T) { // test that it can make a simple csv request // and get the response for it. result := `task,content,result 1,task1,ok 2,task2,fail ` server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Add(runtime.HeaderContentType, runtime.CSVMime) rw.WriteHeader(http.StatusOK) _, _ = rw.Write([]byte(result)) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res bytes.Buffer if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), }) require.NoError(t, err) assert.IsType(t, bytes.Buffer{}, res) actual, ok := res.(bytes.Buffer) require.TrueT(t, ok) assert.EqualT(t, result, actual.String()) } type roundTripperFunc func(*http.Request) (*http.Response, error) func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { return fn(req) } func TestRuntime_CustomTransport(t *testing.T) { rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) result := []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, } rt := New("localhost:3245", "/", []string{"ws", "wss", schemeHTTPS}) rt.Transport = roundTripperFunc(func(req *http.Request) (*http.Response, error) { if req.URL.Scheme != schemeHTTPS { return nil, errors.New("this was not a https request") } assert.EqualT(t, "localhost:3245", req.Host) assert.EqualT(t, "localhost:3245", req.URL.Host) var resp http.Response resp.StatusCode = http.StatusOK resp.Header = make(http.Header) resp.Header.Set("Content-Type", "application/json") buf := bytes.NewBuffer(nil) enc := json.NewEncoder(buf) require.NoError(t, enc.Encode(result)) resp.Body = io.NopCloser(buf) return &resp, nil }) res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/", Schemes: []string{"ws", "wss", schemeHTTPS}, Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res []task if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), }) require.NoError(t, err) assert.IsType(t, []task{}, res) actual, ok := res.([]task) require.TrueT(t, ok) assert.Equal(t, result, actual) } func TestRuntime_CustomCookieJar(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { authenticated := false for _, cookie := range req.Cookies() { if cookie.Name == "sessionid" && cookie.Value == "abc" { authenticated = true } } if !authenticated { username, password, ok := req.BasicAuth() if ok && username == "username" && password == "password" { authenticated = true http.SetCookie(rw, &http.Cookie{ //nolint:gosec // it's okay for a local test cookie Name: "sessionid", Value: "abc", }) } } if authenticated { rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime) rw.WriteHeader(http.StatusOK) jsongen := json.NewEncoder(rw) assert.NoError(t, jsongen.Encode([]task{})) } else { rw.WriteHeader(http.StatusUnauthorized) } })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) rt.Jar, err = cookiejar.New(nil) require.NoError(t, err) submit := func(authInfo runtime.ClientAuthInfoWriter) { _, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/", Params: rwrtr, AuthInfo: authInfo, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, _ runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { return map[string]any{}, nil } return nil, errors.New("generic error") }), }) require.NoError(t, err) } submit(BasicAuth("username", "password")) submit(nil) } func TestRuntime_AuthCanary(t *testing.T) { // test that it can make a simple request // and get the response for it. // defaults all the way down result := []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, } server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.Header.Get(runtime.HeaderAuthorization) != bearerToken { rw.WriteHeader(http.StatusUnauthorized) return } rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime) rw.WriteHeader(http.StatusOK) jsongen := json.NewEncoder(rw) assert.NoError(t, jsongen.Encode(result)) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res []task if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), AuthInfo: BearerToken(token), }) require.NoError(t, err) assert.IsType(t, []task{}, res) actual, ok := res.([]task) require.TrueT(t, ok) assert.Equal(t, result, actual) } // TestPickConsumesMediaType covers the selection rule used by the client // runtime. Two responsibilities: // 1. multipart/form-data preference (issue #286); // 2. producer-capability filter — skip consumes entries with no producer // registered, falling through to the next available match. Only fall // back to first-non-empty when nothing in consumes is registered (so // the gate at runtime.go:551 still emits its diagnostic). func TestPickConsumesMediaType(t *testing.T) { const def = "application/json" defaultProducers := New("example.com", "/", []string{schemeHTTP}).Producers jsonOnly := map[string]runtime.Producer{ runtime.JSONMime: runtime.JSONProducer(), } cases := []struct { name string consumes []string producers map[string]runtime.Producer want string }{ {"empty falls back to default", nil, defaultProducers, def}, {"only empties fall back to default", []string{"", ""}, defaultProducers, def}, {"single entry wins", []string{"text/plain"}, defaultProducers, "text/plain"}, {"first non-empty wins when no multipart", []string{"", runtime.URLencodedFormMime}, defaultProducers, runtime.URLencodedFormMime}, {"multipart preferred when listed first", []string{runtime.MultipartFormMime, runtime.URLencodedFormMime}, defaultProducers, runtime.MultipartFormMime}, {"multipart preferred when listed last", []string{runtime.URLencodedFormMime, runtime.MultipartFormMime}, defaultProducers, runtime.MultipartFormMime}, {"caller's explicit single choice is honored", []string{runtime.URLencodedFormMime}, defaultProducers, runtime.URLencodedFormMime}, {"multipart match is case-insensitive", []string{runtime.URLencodedFormMime, "Multipart/Form-Data"}, defaultProducers, "Multipart/Form-Data"}, // Producer-capability filter (issues #32, #386). {"unregistered first entry falls through to registered second", []string{"application/x-vendor", runtime.JSONMime}, jsonOnly, runtime.JSONMime}, {"all unregistered entries: first non-empty preserved (gate fires)", []string{vendorMime1, vendorMime2}, jsonOnly, vendorMime1}, {"structural mime returns even without producer entry", []string{runtime.URLencodedFormMime}, jsonOnly, runtime.URLencodedFormMime}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert.EqualT(t, tc.want, pickConsumesMediaType(tc.consumes, tc.producers, def)) }) } } func TestRuntime_PickConsumer(t *testing.T) { result := []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, } server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.Header.Get("Content-Type") != runtime.DefaultMime { rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime+charsetUTF8) rw.WriteHeader(http.StatusBadRequest) return } rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime+charsetUTF8) rw.WriteHeader(http.StatusOK) jsongen := json.NewEncoder(rw) _ = jsongen.Encode(result) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetBodyParam(bytes.NewBufferString("hello")) }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodPost, PathPattern: "/", Schemes: []string{schemeHTTP}, ConsumesMediaTypes: []string{runtime.DefaultMime}, Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res []task if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), AuthInfo: BearerToken(token), }) require.NoError(t, err) assert.IsType(t, []task{}, res) actual, ok := res.([]task) require.TrueT(t, ok) assert.Equal(t, result, actual) } func TestRuntime_ContentTypeCanary(t *testing.T) { // test that it can make a simple request // and get the response for it. // defaults all the way down result := []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, } server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.Header.Get(runtime.HeaderAuthorization) != bearerToken { rw.WriteHeader(http.StatusBadRequest) return } rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime+charsetUTF8) rw.WriteHeader(http.StatusOK) jsongen := json.NewEncoder(rw) _ = jsongen.Encode(result) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/", Schemes: []string{schemeHTTP}, Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res []task if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), AuthInfo: BearerToken(token), }) require.NoError(t, err) assert.IsType(t, []task{}, res) actual, ok := res.([]task) require.TrueT(t, ok) assert.Equal(t, result, actual) } func TestRuntime_ChunkedResponse(t *testing.T) { // test that it can make a simple request // and get the response for it. // defaults all the way down result := []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, } server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.Header.Get(runtime.HeaderAuthorization) != bearerToken { rw.WriteHeader(http.StatusBadRequest) return } rw.Header().Add(runtime.HeaderTransferEncoding, "chunked") rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime+charsetUTF8) rw.WriteHeader(http.StatusOK) jsongen := json.NewEncoder(rw) _ = jsongen.Encode(result) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) // specDoc, err := spec.Load("../../fixtures/codegen/todolist.simple.yml") hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/", Schemes: []string{schemeHTTP}, Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res []task if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), AuthInfo: BearerToken(token), }) require.NoError(t, err) assert.IsType(t, []task{}, res) actual, ok := res.([]task) require.TrueT(t, ok) assert.Equal(t, result, actual) } func TestRuntime_DebugValue(t *testing.T) { t.Run("empty DEBUG means Debug is False", func(t *testing.T) { t.Setenv("DEBUG", "") runtime := New("", "/", []string{schemeHTTPS}) assert.FalseT(t, runtime.Debug) }) t.Run("non-Empty DEBUG means Debug is True", func(t *testing.T) { t.Run("with numerical value", func(t *testing.T) { t.Setenv("DEBUG", "1") runtime := New("", "/", []string{schemeHTTPS}) assert.TrueT(t, runtime.Debug) }) t.Run("with boolean value true", func(t *testing.T) { t.Setenv("DEBUG", "true") runtime := New("", "/", []string{schemeHTTPS}) assert.TrueT(t, runtime.Debug) }) t.Run("with boolean value false", func(t *testing.T) { t.Setenv("DEBUG", "false") runtime := New("", "/", []string{schemeHTTPS}) assert.FalseT(t, runtime.Debug) }) t.Run("with string value ", func(t *testing.T) { t.Setenv("DEBUG", "foo") runtime := New("", "/", []string{schemeHTTPS}) assert.TrueT(t, runtime.Debug) }) }) } func TestRuntime_OverrideScheme(t *testing.T) { runtime := New("", "/", []string{schemeHTTPS}) sch := runtime.pickScheme([]string{schemeHTTP}) assert.EqualT(t, schemeHTTPS, sch) } func TestRuntime_OverrideClient(t *testing.T) { client := &http.Client{} runtime := NewWithClient("", "/", []string{schemeHTTPS}, client) var i int runtime.clientOnce.Do(func() { i++ }) assert.Equal(t, client, runtime.client) assert.EqualT(t, 0, i) } type overrideRoundTripper struct { overridden bool } func (o *overrideRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) { o.overridden = true res := new(http.Response) res.StatusCode = http.StatusOK res.Body = io.NopCloser(bytes.NewBufferString("OK")) return res, nil } func TestRuntime_OverrideClientOperation(t *testing.T) { client := &http.Client{} rt := NewWithClient("", "/", []string{schemeHTTPS}, client) var i int rt.clientOnce.Do(func() { i++ }) assert.Equal(t, client, rt.client) assert.EqualT(t, 0, i) client2 := new(http.Client) var transport = &overrideRoundTripper{} client2.Transport = transport require.NotEqual(t, client, client2) _, err := rt.Submit(&runtime.ClientOperation{ Client: client2, Params: runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }), Reader: runtime.ClientResponseReaderFunc(func(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return map[string]any{}, nil }), }) require.NoError(t, err) assert.TrueT(t, transport.overridden) } func TestRuntime_PreserveTrailingSlash(t *testing.T) { var redirected bool server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime+charsetUTF8) if req.URL.Path == "/api/tasks" { redirected = true return } if req.URL.Path == "/api/tasks/" { rw.WriteHeader(http.StatusOK) } })) defer server.Close() hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) rwrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) _, err = rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/api/tasks/", Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, _ runtime.Consumer) (any, error) { if redirected { return nil, errors.New("expected Submit to preserve trailing slashes - this caused a redirect") } if response.Code() == http.StatusOK { return map[string]any{}, nil } return nil, errors.New("generic error") }), }) require.NoError(t, err) } func TestRuntime_FallbackConsumer(t *testing.T) { result := `W3siY29tcGxldGVkIjpmYWxzZSwiY29udGVudCI6ImRHRnpheUF4SUdOdmJuUmxiblE9IiwiaWQiOjF9XQ==` server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Add(runtime.HeaderContentType, "application/x-task") rw.WriteHeader(http.StatusOK) _, _ = rw.Write([]byte(result)) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetBodyParam(bytes.NewBufferString("hello")) }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) // without the fallback consumer _, err = rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodPost, PathPattern: "/", Schemes: []string{schemeHTTP}, ConsumesMediaTypes: []string{runtime.DefaultMime}, Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res []byte if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), }) require.Error(t, err) assert.EqualT(t, `no consumer: "application/x-task"`, err.Error()) // add the fallback consumer rt.Consumers["*/*"] = rt.Consumers[runtime.DefaultMime] res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Method: http.MethodPost, PathPattern: "/", Schemes: []string{schemeHTTP}, ConsumesMediaTypes: []string{runtime.DefaultMime}, Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res []byte if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), }) require.NoError(t, err) assert.IsType(t, []byte{}, res) actual, ok := res.([]byte) require.TrueT(t, ok) assert.EqualValues(t, result, actual) } func TestRuntime_AuthHeaderParamDetected(t *testing.T) { // test that it can make a simple request // and get the response for it. // defaults all the way down result := []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, } server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.Header.Get(runtime.HeaderAuthorization) != bearerToken { rw.WriteHeader(http.StatusUnauthorized) return } rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime) rw.WriteHeader(http.StatusOK) jsongen := json.NewEncoder(rw) _ = jsongen.Encode(result) })) defer server.Close() rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetHeaderParam(runtime.HeaderAuthorization, bearerToken) }) hu, err := url.Parse(server.URL) require.NoError(t, err) rt := New(hu.Host, "/", []string{schemeHTTP}) rt.DefaultAuthentication = BearerToken("not-the-super-secret-token") res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Params: rwrtr, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { var res []task if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("generic error") }), }) require.NoError(t, err) assert.IsType(t, []task{}, res) actual, ok := res.([]task) require.TrueT(t, ok) assert.Equal(t, result, actual) } func TestRuntime_Timeout(t *testing.T) { //nolint:maintidx // linter evaluates the total lines of code, which is misleading const ( // these values should be sufficient for most CI engines clientTimeout time.Duration = 25 * time.Millisecond clientNoTimeout time.Duration = 250 * time.Millisecond ctxError = "context deadline exceeded" ) result := []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, } signedContext := func(value string) context.Context { return context.WithValue(context.Background(), rtKey, value) } requestWriter := func(timeout time.Duration) runtime.ClientRequestWriter { // this writer sets the timeout parameter of the ClientRequest return runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetTimeout(timeout) }) } requestReader := runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() != http.StatusOK { return nil, errors.New("generic error") } var res []task if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil }) t.Run("with timeout specified as a request parameter, no operation context", func(t *testing.T) { host, cleaner := serverBuilder(t, result) t.Cleanup(cleaner) rt := New(host, "/", []string{schemeHTTP}) rt.Context = signedContext("test") rt.Transport = testContextTransport(t, true, true, "test") t.Run("should not time out", func(t *testing.T) { res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: nil, Params: requestWriter(clientNoTimeout), Reader: requestReader, }) require.NoError(t, err) assertResult(result)(t, res) }) t.Run("should time out", func(t *testing.T) { _, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: nil, Params: requestWriter(clientTimeout), Reader: requestReader, }) require.Error(t, err) require.ErrorContains(t, err, ctxError) }) }) t.Run("with timeout specified as a request parameter, no context at all", func(t *testing.T) { host, cleaner := serverBuilder(t, result) t.Cleanup(cleaner) rt := New(host, "/", []string{schemeHTTP}) rt.Context = nil rt.Transport = testContextTransport(t, true, false, "") t.Run("should not time out", func(t *testing.T) { res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: nil, Params: requestWriter(clientNoTimeout), Reader: requestReader, }) require.NoError(t, err) assertResult(result)(t, res) }) t.Run("should time out", func(t *testing.T) { _, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: nil, Params: requestWriter(clientTimeout), Reader: requestReader, }) require.Error(t, err) require.ErrorContains(t, err, ctxError) }) }) t.Run("with inherited operation context, timeout specified as operation context, request timeout set to 0", func(t *testing.T) { host, cleaner := serverBuilder(t, result) t.Cleanup(cleaner) rt := New(host, "/", []string{schemeHTTP}) rt.Context = signedContext("test") rt.Transport = testContextTransport(t, true, true, "test") t.Run("should not time out", func(t *testing.T) { operationCtx, cancel := context.WithTimeout(rt.Context, clientNoTimeout) defer cancel() res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: operationCtx, Params: requestWriter(0), Reader: requestReader, }) require.NoError(t, err) assertResult(result)(t, res) }) t.Run("should time out", func(t *testing.T) { operationCtx, cancel := context.WithTimeout(rt.Context, clientTimeout) defer cancel() _, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: operationCtx, Params: requestWriter(0), Reader: requestReader, }) require.Error(t, err) require.ErrorContains(t, err, ctxError) }) }) t.Run("with a fresh operation context, timeout specified as operation context, request timeout set to 0", func(t *testing.T) { host, cleaner := serverBuilder(t, result) t.Cleanup(cleaner) rt := New(host, "/", []string{schemeHTTP}) rt.Context = nil rt.Transport = testContextTransport(t, true, false, "") t.Run("should not time out", func(t *testing.T) { operationCtx, cancel := context.WithTimeout(context.Background(), clientNoTimeout) defer cancel() res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: operationCtx, Params: requestWriter(0), Reader: requestReader, }) require.NoError(t, err) assertResult(result)(t, res) }) t.Run("should time out", func(t *testing.T) { operationCtx, cancel := context.WithTimeout(context.Background(), clientTimeout) defer cancel() _, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: operationCtx, Params: requestWriter(0), Reader: requestReader, }) require.Error(t, err) require.ErrorContains(t, err, ctxError) }) }) t.Run("with an hypothetical timeout specified as runtime context, no operation context", func(t *testing.T) { // in real life, the runtime context may be cancellable for other reasons than timeout host, cleaner := serverBuilder(t, result) t.Cleanup(cleaner) t.Run("should not time out", func(t *testing.T) { rt := New(host, "/", []string{schemeHTTP}) ctx, cancel := context.WithTimeout(signedContext("test"), clientNoTimeout) defer cancel() rt.Context = ctx rt.Transport = testContextTransport(t, true, true, "test") res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: nil, Params: requestWriter(0), Reader: requestReader, }) require.NoError(t, err) assertResult(result)(t, res) }) t.Run("should time out", func(t *testing.T) { rt := New(host, "/", []string{schemeHTTP}) ctx, cancel := context.WithTimeout(signedContext("test"), clientTimeout) defer cancel() rt.Context = ctx rt.Transport = testContextTransport(t, true, true, "test") _, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: nil, Params: requestWriter(0), Reader: requestReader, }) require.Error(t, err) require.ErrorContains(t, err, ctxError) }) }) t.Run("with multiple timeouts set, shortest wins", func(t *testing.T) { host, cleaner := serverBuilder(t, result) t.Cleanup(cleaner) rt := New(host, "/", []string{schemeHTTP}) runtimeCtx, cancelRuntime := context.WithTimeout(signedContext("test"), clientNoTimeout) rt.Context = runtimeCtx defer cancelRuntime() rt.Transport = testContextTransport(t, true, true, "test") t.Run("should not time out", func(t *testing.T) { operationCtx, cancelOperation := context.WithTimeout( signedContext("test"), serverDelay+(clientNoTimeout-serverDelay)/2, ) defer cancelOperation() res, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: operationCtx, Params: requestWriter(serverDelay + (clientNoTimeout-serverDelay)/3), Reader: requestReader, }) require.NoError(t, err) assertResult(result)(t, res) }) t.Run("should time out on operation context deadline", func(t *testing.T) { // NOTE: we'll be able to catch more precisely which context was canceled // in go1.21 and context.WithTimeoutCause. operationCtx, cancelOperation := context.WithTimeout( signedContext("test"), serverDelay-(clientNoTimeout-serverDelay)/4, // this one times out ) defer cancelOperation() _, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: operationCtx, Params: requestWriter( serverDelay + (clientNoTimeout-serverDelay)/4, ), Reader: requestReader, }) require.Error(t, err) require.ErrorContains(t, err, ctxError) }) t.Run("should time out on operation timeout param", func(t *testing.T) { operationCtx, cancelOperation := context.WithTimeout( signedContext("test"), serverDelay+(clientNoTimeout-serverDelay)/2, ) defer cancelOperation() _, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: operationCtx, Params: requestWriter( serverDelay - (clientNoTimeout-serverDelay)/4, // this one times out ), Reader: requestReader, }) require.Error(t, err) require.ErrorContains(t, err, ctxError) }) }) t.Run("with no context, explicit infinite wait", func(t *testing.T) { host, cleaner := serverBuilder(t, result) t.Cleanup(cleaner) rt := New(host, "/", []string{schemeHTTP}) rt.Context = signedContext("test") rt.Transport = testContextTransport(t, false, true, "test") // verify that no deadline is passed to the emitted context t.Run("should not time out", func(t *testing.T) { resp, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: nil, Params: requestWriter(0), Reader: requestReader, }) require.NoError(t, err) assertResult(result)(t, resp) }) }) t.Run("with no context, request uses the default timeout", func(t *testing.T) { requestEmptyWriter := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }) host, cleaner := serverBuilder(t, result) t.Cleanup(cleaner) rt := New(host, "/", []string{schemeHTTP}) rt.Context = signedContext("test") rt.Transport = testDefaultsInTransport(t, "test") t.Run("should not time out", func(t *testing.T) { resp, err := rt.Submit(&runtime.ClientOperation{ ID: operationID, Context: nil, Params: requestEmptyWriter, // leaves defaults Reader: requestReader, }) require.NoError(t, err) assertResult(result)(t, resp) }) }) } type testReqFn func(*testing.T, *http.Request) type testRoundTripper struct { tr http.RoundTripper testFn testReqFn testHarness *testing.T } func (t *testRoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) { t.testFn(t.testHarness, req) return t.tr.RoundTrip(req) } func TestGetBodyCallsBeforeRoundTrip(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusCreated) _, err := rw.Write([]byte("test result")) assert.NoError(t, err) })) defer server.Close() hu, err := url.Parse(server.URL) require.NoError(t, err) client := http.DefaultClient transport := http.DefaultTransport client.Transport = &testRoundTripper{ tr: transport, testHarness: t, testFn: func(t *testing.T, req *http.Request) { // Read the body once before sending the request body, e := req.GetBody() require.NoError(t, e) bodyContent, e := io.ReadAll(io.Reader(body)) require.NoError(t, e) require.Len(t, bodyContent, int(req.ContentLength)) require.EqualT(t, "\"test body\"\n", string(bodyContent)) // Read the body a second time before sending the request body, e = req.GetBody() require.NoError(t, e) bodyContent, e = io.ReadAll(io.Reader(body)) require.NoError(t, e) require.Len(t, bodyContent, int(req.ContentLength)) require.EqualT(t, "\"test body\"\n", string(bodyContent)) }, } rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetBodyParam("test body") }) operation := &runtime.ClientOperation{ ID: "getSites", Method: http.MethodPost, PathPattern: "/", Params: rwrtr, Client: client, Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusCreated { var res string if e := consumer.Consume(response.Body(), &res); e != nil { return nil, e } return res, nil } return nil, errors.New("unexpected error code") }), } openAPIClient := New(hu.Host, "/", []string{schemeHTTP}) res, err := openAPIClient.Submit(operation) require.NoError(t, err) actual, ok := res.(string) require.TrueT(t, ok) require.EqualT(t, "test result", actual) } func isContextSigned(ctx context.Context, value string) bool { v, ok := ctx.Value(rtKey).(string) return ok && v == value } func testContextTransport(t *testing.T, hasTimeout, expectSigned bool, value string) http.RoundTripper { // inject a round tripper to check the context in the request about to be emitted return roundTripperFunc(func(req *http.Request) (*http.Response, error) { ctx := req.Context() t.Run("request context should propagate value", func(t *testing.T) { assert.EqualT(t, expectSigned, isContextSigned(ctx, value), "expected the request context to inherit values") }) t.Run(fmt.Sprintf("request context should have a deadline %t", hasTimeout), func(t *testing.T) { _, hasDeadline := ctx.Deadline() assert.EqualTf(t, hasTimeout, hasDeadline, "expected request context to have a deadline") }) return http.DefaultTransport.RoundTrip(req) }) } func testDefaultsInTransport(t *testing.T, value string) http.RoundTripper { // inject a round tripper to check the context in the request about to be emitted return roundTripperFunc(func(req *http.Request) (*http.Response, error) { ctx := req.Context() t.Run("request context should propagate value", func(t *testing.T) { assert.TrueT(t, isContextSigned(ctx, value), "expected the request context to inherit values") }) t.Run("request context should have a default deadline", func(t *testing.T) { deadline, hasDeadline := ctx.Deadline() assert.TrueT(t, hasDeadline, "expected request context to have a deadline") remainingDuration := time.Until(deadline).Seconds() assert.InDeltaTf(t, DefaultTimeout.Seconds(), remainingDuration, 1.0, "expected timeout to be set to DefaultTimeout") }) return http.DefaultTransport.RoundTrip(req) }) } func assertResult(result []task) func(testing.TB, any) { return func(t testing.TB, res any) { assert.IsType(t, []task{}, res) actual, ok := res.([]task) require.TrueT(t, ok) assert.Equal(t, result, actual) } } const serverDelay = 100 * time.Millisecond func serverBuilder(t testing.TB, result []task) (string, func()) { delay := serverDelay server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { ctx := req.Context() timer := time.NewTimer(delay) select { case <-ctx.Done(): http.Error(rw, ctx.Err().Error(), http.StatusInternalServerError) return case <-timer.C: rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime) rw.WriteHeader(http.StatusOK) jsongen := json.NewEncoder(rw) _ = jsongen.Encode(result) return } })) hu, err := url.Parse(server.URL) require.NoError(t, err) return hu.Host, server.Close } go-openapi-runtime-decad8f/client/runtime_tls_test.go000066400000000000000000000201701520232310000232660ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "crypto" "crypto/ed25519" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/json" "errors" "math/big" "net/http" "net/http/httptest" "net/url" goruntime "runtime" "testing" "time" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestRuntimeTLSOptions(t *testing.T) { fixtures := newTLSFixtures(t) t.Run("with TLSAuthConfig configured with files", func(t *testing.T) { opts := TLSClientOptions{ CA: fixtures.RSA.CAFile, Key: fixtures.RSA.KeyFile, Certificate: fixtures.RSA.CertFile, ServerName: fixtures.Subject, } cfg, err := TLSClientAuth(opts) require.NoError(t, err) require.NotNil(t, cfg) assert.Len(t, cfg.Certificates, 1) assert.NotNil(t, cfg.RootCAs) assert.EqualT(t, fixtures.Subject, cfg.ServerName) }) t.Run("with loaded TLS material", func(t *testing.T) { t.Run("with TLSConfig from loaded RSA key/cert pair", func(t *testing.T) { opts := TLSClientOptions{ LoadedKey: fixtures.RSA.LoadedKey, LoadedCertificate: fixtures.RSA.LoadedCert, } cfg, err := TLSClientAuth(opts) require.NoError(t, err) require.NotNil(t, cfg) assert.Len(t, cfg.Certificates, 1) }) t.Run("with TLSAuthConfig configured with loaded TLS Elliptic Curve key/certificate", func(t *testing.T) { opts := TLSClientOptions{ LoadedKey: fixtures.ECDSA.LoadedKey, LoadedCertificate: fixtures.ECDSA.LoadedCert, } cfg, err := TLSClientAuth(opts) require.NoError(t, err) require.NotNil(t, cfg) assert.Len(t, cfg.Certificates, 1) }) t.Run("with TLSAuthConfig configured with loaded Ed25519 key/certificate", func(t *testing.T) { edKey, edCert := newEd25519KeyCert(t) opts := TLSClientOptions{ LoadedKey: edKey, LoadedCertificate: edCert, } cfg, err := TLSClientAuth(opts) require.NoError(t, err) require.NotNil(t, cfg) assert.Len(t, cfg.Certificates, 1) }) t.Run("with TLSAuthConfig configured with loaded Certificate Authority", func(t *testing.T) { opts := TLSClientOptions{ LoadedCA: fixtures.RSA.LoadedCA, } cfg, err := TLSClientAuth(opts) require.NoError(t, err) require.NotNil(t, cfg) assert.NotNil(t, cfg.RootCAs) }) t.Run("with TLSAuthConfig configured with loaded CA pool", func(t *testing.T) { pool := x509.NewCertPool() pool.AddCert(fixtures.RSA.LoadedCA) opts := TLSClientOptions{ LoadedCAPool: pool, } cfg, err := TLSClientAuth(opts) require.NoError(t, err) require.NotNil(t, cfg) require.NotNil(t, cfg.RootCAs) require.Equal(t, pool, cfg.RootCAs) }) t.Run("with TLSAuthConfig configured with loaded CA and CA pool", func(t *testing.T) { pool := systemCAPool(t) opts := TLSClientOptions{ LoadedCAPool: pool, LoadedCA: fixtures.RSA.LoadedCA, } cfg, err := TLSClientAuth(opts) require.NoError(t, err) require.NotNil(t, cfg) require.NotNil(t, cfg.RootCAs) // verify that the CA cert is indeed valid against the configured pool. // NOTE: fixtures may be expired certs, but may validate with a fixed date in the past. chains, err := fixtures.RSA.LoadedCA.Verify(x509.VerifyOptions{ Roots: cfg.RootCAs, CurrentTime: time.Date(2027, 1, 1, 1, 1, 1, 1, time.UTC), }) require.NoError(t, err) require.NotEmpty(t, chains) }) t.Run("with TLSAuthConfig with VerifyPeer option", func(t *testing.T) { verify := func(_ [][]byte, _ [][]*x509.Certificate) error { return nil } opts := TLSClientOptions{ InsecureSkipVerify: true, VerifyPeerCertificate: verify, } cfg, err := TLSClientAuth(opts) require.NoError(t, err) require.NotNil(t, cfg) assert.TrueT(t, cfg.InsecureSkipVerify) assert.NotNil(t, cfg.VerifyPeerCertificate) }) }) } func TestRuntimeManualCertificateValidation(t *testing.T) { // test manual verification of server certificates // against root certificate on client side. // // The client compares the received cert against the root cert, // explicitly omitting DNSName check. fixtures := newTLSFixtures(t) result := []task{ {false, taskOneContent, 1}, {false, taskTwoContent, 2}, } host, clean := testTLSServer(t, fixtures, result) t.Cleanup(clean) var certVerifyCalled bool client := testTLSClient(t, fixtures, &certVerifyCalled) rt := NewWithClient(host, "/", []string{schemeHTTPS}, client) var received []task operation := &runtime.ClientOperation{ ID: operationID, Method: http.MethodGet, PathPattern: "/", Params: runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error { return nil }), Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { if response.Code() == http.StatusOK { if e := consumer.Consume(response.Body(), &received); e != nil { return nil, e } return result, nil } return nil, errors.New("generic error") }), } resp, err := rt.Submit(operation) require.NoError(t, err) require.NotEmpty(t, resp) assert.IsType(t, []task{}, resp) assert.TrueTf(t, certVerifyCalled, "the client cert verification has not been called") assert.Equal(t, result, received) } func testTLSServer(t testing.TB, fixtures *tlsFixtures, expectedResult []task) (string, func()) { server := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime) rw.WriteHeader(http.StatusOK) jsongen := json.NewEncoder(rw) assert.NoError(t, jsongen.Encode(expectedResult)) })) // create server tls config serverCACertPool := x509.NewCertPool() serverCACertPool.AddCert(fixtures.Server.LoadedCA) // load server certs serverCert, err := tls.LoadX509KeyPair( fixtures.Server.CertFile, fixtures.Server.KeyFile, ) require.NoError(t, err) server.TLS = &tls.Config{ RootCAs: serverCACertPool, MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{serverCert}, } require.NoError(t, err) server.StartTLS() testURL, err := url.Parse(server.URL) require.NoError(t, err) return testURL.Host, server.Close } func testTLSClient(t testing.TB, fixtures *tlsFixtures, verifyCalled *bool) *http.Client { client, err := TLSClient(TLSClientOptions{ InsecureSkipVerify: true, VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error { *verifyCalled = true caCertPool := x509.NewCertPool() caCertPool.AddCert(fixtures.RSA.LoadedCA) opts := x509.VerifyOptions{ Roots: caCertPool, CurrentTime: time.Date(2027, time.July, 1, 1, 1, 1, 1, time.UTC), } cert, e := x509.ParseCertificate(rawCerts[0]) if e != nil { return e } _, e = cert.Verify(opts) return e }, }) require.NoError(t, err) return client } type ( tlsFixtures struct { RSA tlsFixture ECDSA tlsFixture Server tlsFixture Subject string } tlsFixture struct { LoadedCA *x509.Certificate LoadedCert *x509.Certificate LoadedKey crypto.PrivateKey CAFile string KeyFile string CertFile string } ) func newEd25519KeyCert(t testing.TB) (ed25519.PrivateKey, *x509.Certificate) { t.Helper() pub, priv, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) template := &x509.Certificate{ SerialNumber: big.NewInt(42), Subject: pkix.Name{CommonName: "ed25519-test"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } der, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv) require.NoError(t, err) cert, err := x509.ParseCertificate(der) require.NoError(t, err) return priv, cert } func systemCAPool(t testing.TB) *x509.CertPool { if goruntime.GOOS == "windows" { // Windows doesn't have the system cert pool. return x509.NewCertPool() } pool, err := x509.SystemCertPool() require.NoError(t, err) return pool } go-openapi-runtime-decad8f/client/tls.go000066400000000000000000000155311520232310000204710ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package client import ( "crypto" "crypto/tls" "crypto/x509" "encoding/pem" "fmt" "net/http" "os" ) // TLSClientOptions to configure client authentication with mutual TLS. type TLSClientOptions struct { // Certificate is the path to a PEM-encoded certificate to be used for // client authentication. If set then Key must also be set. Certificate string // LoadedCertificate is the certificate to be used for client authentication. // This field is ignored if Certificate is set. If this field is set, LoadedKey // is also required. LoadedCertificate *x509.Certificate // Key is the path to an unencrypted PEM-encoded private key for client // authentication. This field is required if Certificate is set. Key string // LoadedKey is the key for client authentication. This field is required if // LoadedCertificate is set. LoadedKey crypto.PrivateKey // CA is a path to a PEM-encoded certificate that specifies the root certificate // to use when validating the TLS certificate presented by the server. If this field // (and LoadedCA) is not set, the system certificate pool is used. This field is ignored if LoadedCA // is set. CA string // LoadedCA specifies the root certificate to use when validating the server's TLS certificate. // If this field (and CA) is not set, the system certificate pool is used. LoadedCA *x509.Certificate // LoadedCAPool specifies a pool of RootCAs to use when validating the server's TLS certificate. // If set, it will be combined with the other loaded certificates (see LoadedCA and CA). // If neither LoadedCA or CA is set, the provided pool will override the system // certificate pool. // // The caller must not use the supplied pool after calling TLSClientAuth. LoadedCAPool *x509.CertPool // ServerName specifies the hostname to use when verifying the server certificate. // If this field is set then InsecureSkipVerify will be ignored and treated as // false. ServerName string // InsecureSkipVerify controls whether the certificate chain and hostname presented // by the server are validated. If true, any certificate is accepted. InsecureSkipVerify bool // VerifyPeerCertificate, if not nil, is called after normal // certificate verification. It receives the raw ASN.1 certificates // provided by the peer and also any verified chains that normal processing found. // If it returns a non-nil error, the handshake is aborted and that error results. // // If normal verification fails then the handshake will abort before // considering this callback. If normal verification is disabled by // setting InsecureSkipVerify then this callback will be considered but // the verifiedChains argument will always be nil. VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error // VerifyConnection, if not nil, is called after normal certificate // verification and after [TLSClientOptions.VerifyPeerCertificate] by either a TLS client or // server. It receives the [tls.ConnectionState] which may be inspected. // // Unlike VerifyPeerCertificate, this callback is invoked on every // connection, including resumed ones, making it suitable for checks // that must always apply (e.g. certificate pinning). // // If it returns a non-nil error, the handshake is aborted and that error results. VerifyConnection func(tls.ConnectionState) error // SessionTicketsDisabled may be set to true to disable session ticket and // PSK (resumption) support. Note that on clients, session ticket support is // also disabled if ClientSessionCache is nil. SessionTicketsDisabled bool // ClientSessionCache is a cache of ClientSessionState entries for TLS // session resumption. It is only used by clients. ClientSessionCache tls.ClientSessionCache // Prevents callers using unkeyed fields. _ struct{} } // TLSClientAuth creates a [tls.Config] for mutual auth. func TLSClientAuth(opts TLSClientOptions) (*tls.Config, error) { // create client tls config cfg := &tls.Config{ MinVersion: tls.VersionTLS12, } // load client cert if specified if opts.Certificate != "" { cert, err := tls.LoadX509KeyPair(opts.Certificate, opts.Key) if err != nil { return nil, fmt.Errorf("tls client cert: %w", err) } cfg.Certificates = []tls.Certificate{cert} } else if opts.LoadedCertificate != nil { block := pem.Block{Type: "CERTIFICATE", Bytes: opts.LoadedCertificate.Raw} certPem := pem.EncodeToMemory(&block) // PKCS#8 covers RSA, ECDSA, Ed25519, X25519 (the key types tls.X509KeyPair // understands) and pairs with the canonical "PRIVATE KEY" PEM label. keyBytes, err := x509.MarshalPKCS8PrivateKey(opts.LoadedKey) if err != nil { return nil, fmt.Errorf("tls client priv key: %w", err) } block = pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes} keyPem := pem.EncodeToMemory(&block) cert, err := tls.X509KeyPair(certPem, keyPem) if err != nil { return nil, fmt.Errorf("tls client cert: %w", err) } cfg.Certificates = []tls.Certificate{cert} } cfg.InsecureSkipVerify = opts.InsecureSkipVerify cfg.VerifyPeerCertificate = opts.VerifyPeerCertificate cfg.VerifyConnection = opts.VerifyConnection cfg.SessionTicketsDisabled = opts.SessionTicketsDisabled cfg.ClientSessionCache = opts.ClientSessionCache // When no CA certificate is provided, default to the system cert pool // that way when a request is made to a server known by the system trust store, // the name is still verified switch { case opts.LoadedCA != nil: caCertPool := basePool(opts.LoadedCAPool) caCertPool.AddCert(opts.LoadedCA) cfg.RootCAs = caCertPool case opts.CA != "": // load ca cert caCert, err := os.ReadFile(opts.CA) if err != nil { return nil, fmt.Errorf("tls client ca: %w", err) } caCertPool := basePool(opts.LoadedCAPool) caCertPool.AppendCertsFromPEM(caCert) cfg.RootCAs = caCertPool case opts.LoadedCAPool != nil: cfg.RootCAs = opts.LoadedCAPool } // apply servername override if opts.ServerName != "" { cfg.InsecureSkipVerify = false cfg.ServerName = opts.ServerName } return cfg, nil } // TLSTransport creates a [http.RoundTripper] for a client transport,suitable for mutual TLS auth. func TLSTransport(opts TLSClientOptions) (http.RoundTripper, error) { cfg, err := TLSClientAuth(opts) if err != nil { return nil, err } return &http.Transport{TLSClientConfig: cfg}, nil } // TLSClient creates a [http.Client] for mutual auth. func TLSClient(opts TLSClientOptions) (*http.Client, error) { transport, err := TLSTransport(opts) if err != nil { return nil, err } return &http.Client{Transport: transport}, nil } // basePool returns pool if non-nil; otherwise it returns a new empty cert pool. // // Clones the pool provided up front by the caller. func basePool(pool *x509.CertPool) *x509.CertPool { if pool == nil { return x509.NewCertPool() } return pool.Clone() } go-openapi-runtime-decad8f/client_auth_info.go000066400000000000000000000012761520232310000217240ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import "github.com/go-openapi/strfmt" // A ClientAuthInfoWriterFunc converts a function to a request writer interface. type ClientAuthInfoWriterFunc func(ClientRequest, strfmt.Registry) error // AuthenticateRequest adds authentication data to the request. func (fn ClientAuthInfoWriterFunc) AuthenticateRequest(req ClientRequest, reg strfmt.Registry) error { return fn(req, reg) } // A ClientAuthInfoWriter implementor knows how to write authentication info to a request. type ClientAuthInfoWriter interface { AuthenticateRequest(ClientRequest, strfmt.Registry) error } go-openapi-runtime-decad8f/client_operation.go000066400000000000000000000017351520232310000217500ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "context" "net/http" ) // ClientOperation represents the context for a swagger operation to be submitted to the transport. type ClientOperation struct { ID string Method string PathPattern string ProducesMediaTypes []string ConsumesMediaTypes []string Schemes []string AuthInfo ClientAuthInfoWriter Params ClientRequestWriter Reader ClientResponseReader Context context.Context //nolint:containedctx // we precisely want this type to contain the request context Client *http.Client } // A ClientTransport implementor knows how to submit Request objects to some destination. type ClientTransport interface { // Submit(string, RequestWriter, ResponseReader, AuthInfoWriter) (interface{}, error) Submit(*ClientOperation) (any, error) } go-openapi-runtime-decad8f/client_request.go000066400000000000000000000063701520232310000214400ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "io" "net/http" "net/url" "time" "github.com/go-openapi/strfmt" ) // ClientRequestWriterFunc converts a function to a request writer interface. type ClientRequestWriterFunc func(ClientRequest, strfmt.Registry) error // WriteToRequest adds data to the request. func (fn ClientRequestWriterFunc) WriteToRequest(req ClientRequest, reg strfmt.Registry) error { return fn(req, reg) } // ClientRequestWriter is an interface for things that know how to write to a request. type ClientRequestWriter interface { WriteToRequest(ClientRequest, strfmt.Registry) error } // ClientRequest is an interface for things that know how to // add information to a swagger client request. type ClientRequest interface { //nolint:interfacebloat // a swagger-capable request is quite rich, hence the many getter/setters SetHeaderParam(string, ...string) error GetHeaderParams() http.Header SetQueryParam(string, ...string) error SetFormParam(string, ...string) error SetPathParam(string, string) error GetQueryParams() url.Values SetFileParam(string, ...NamedReadCloser) error SetBodyParam(any) error SetTimeout(time.Duration) error GetMethod() string GetPath() string GetBody() []byte GetBodyParam() any GetFileParam() map[string][]NamedReadCloser } // NamedReadCloser represents a named ReadCloser interface. type NamedReadCloser interface { io.ReadCloser Name() string } // NamedReader creates a [NamedReadCloser] for use as file upload. func NamedReader(name string, rdr io.Reader) NamedReadCloser { rc, ok := rdr.(io.ReadCloser) if !ok { rc = io.NopCloser(rdr) } return &namedReadCloser{ name: name, cr: rc, } } type namedReadCloser struct { name string cr io.ReadCloser } func (n *namedReadCloser) Close() error { return n.cr.Close() } func (n *namedReadCloser) Read(p []byte) (int, error) { return n.cr.Read(p) } func (n *namedReadCloser) Name() string { return n.name } type TestClientRequest struct { Headers http.Header Body any } func (t *TestClientRequest) SetHeaderParam(name string, values ...string) error { if t.Headers == nil { t.Headers = make(http.Header) } t.Headers.Set(name, values[0]) return nil } func (t *TestClientRequest) SetQueryParam(_ string, _ ...string) error { return nil } func (t *TestClientRequest) SetFormParam(_ string, _ ...string) error { return nil } func (t *TestClientRequest) SetPathParam(_ string, _ string) error { return nil } func (t *TestClientRequest) SetFileParam(_ string, _ ...NamedReadCloser) error { return nil } func (t *TestClientRequest) SetBodyParam(body any) error { t.Body = body return nil } func (t *TestClientRequest) SetTimeout(time.Duration) error { return nil } func (t *TestClientRequest) GetQueryParams() url.Values { return nil } func (t *TestClientRequest) GetMethod() string { return "" } func (t *TestClientRequest) GetPath() string { return "" } func (t *TestClientRequest) GetBody() []byte { return nil } func (t *TestClientRequest) GetBodyParam() any { return t.Body } func (t *TestClientRequest) GetFileParam() map[string][]NamedReadCloser { return nil } func (t *TestClientRequest) GetHeaderParams() http.Header { return t.Headers } go-openapi-runtime-decad8f/client_request_test.go000066400000000000000000000013431520232310000224720ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "testing" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestRequestWriterFunc(t *testing.T) { hand := ClientRequestWriterFunc(func(r ClientRequest, _ strfmt.Registry) error { _ = r.SetHeaderParam("Blah", "blahblah") _ = r.SetBodyParam(struct{ Name string }{"Adriana"}) return nil }) tr := new(TestClientRequest) _ = hand.WriteToRequest(tr, nil) assert.EqualT(t, "blahblah", tr.Headers.Get("Blah")) body, ok := tr.Body.(struct{ Name string }) require.TrueT(t, ok) assert.EqualT(t, "Adriana", body.Name) } go-openapi-runtime-decad8f/client_response.go000066400000000000000000000060501520232310000216010ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "encoding/json" "fmt" "io" "strings" ) // A ClientResponse represents a client response. // // This bridges between responses obtained from different transports. type ClientResponse interface { Code() int Message() string GetHeader(string) string GetHeaders(string) []string Body() io.ReadCloser } // A ClientResponseReaderFunc turns a function into a [ClientResponseReader] interface implementation. type ClientResponseReaderFunc func(ClientResponse, Consumer) (any, error) // ReadResponse reads the response. func (read ClientResponseReaderFunc) ReadResponse(resp ClientResponse, consumer Consumer) (any, error) { return read(resp, consumer) } // A ClientResponseReader is an interface for things want to read a response. // An application of this is to create structs from response values. type ClientResponseReader interface { ReadResponse(ClientResponse, Consumer) (any, error) } // APIError wraps an error model and captures the status code. type APIError struct { OperationName string Response any Code int } // NewAPIError creates a new API error. func NewAPIError(opName string, payload any, code int) *APIError { return &APIError{ OperationName: opName, Response: payload, Code: code, } } // sanitizer ensures that single quotes are escaped. var sanitizer = strings.NewReplacer(`\`, `\\`, `'`, `\'`) func (o *APIError) Error() string { var resp []byte if err, ok := o.Response.(error); ok { resp = []byte("'" + sanitizer.Replace(err.Error()) + "'") } else { resp, _ = json.Marshal(o.Response) //nolint:errchkjson // error swallowed as this is our last best effort attempt } return fmt.Sprintf("%s (status %d): %s", o.OperationName, o.Code, resp) } func (o *APIError) String() string { return o.Error() } // IsSuccess returns true when this API response returns a 2xx status code. func (o *APIError) IsSuccess() bool { const statusOK = 2 return o.Code/100 == statusOK } // IsRedirect returns true when this API response returns a 3xx status code. func (o *APIError) IsRedirect() bool { const statusRedirect = 3 return o.Code/100 == statusRedirect } // IsClientError returns true when this API response returns a 4xx status code. func (o *APIError) IsClientError() bool { const statusClientError = 4 return o.Code/100 == statusClientError } // IsServerError returns true when this API response returns a 5xx status code. func (o *APIError) IsServerError() bool { const statusServerError = 5 return o.Code/100 == statusServerError } // IsCode returns true when this API response returns a given status code. func (o *APIError) IsCode(code int) bool { return o.Code == code } // A ClientResponseStatus is a common interface implemented by all responses on the generated code // You can use this to treat any client response based on status code. type ClientResponseStatus interface { IsSuccess() bool IsRedirect() bool IsClientError() bool IsServerError() bool IsCode(int) bool } go-openapi-runtime-decad8f/client_response_test.go000066400000000000000000000061471520232310000226470ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "bytes" "errors" "io" "io/fs" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) type response struct { } func (r response) Code() int { return 490 } func (r response) Message() string { return "the message" } func (r response) GetHeader(_ string) string { return "the header" } func (r response) GetHeaders(_ string) []string { return []string{"the headers", "the headers2"} } func (r response) Body() io.ReadCloser { return io.NopCloser(bytes.NewBufferString("the content")) } func TestResponseReaderFunc(t *testing.T) { var actual struct { Header, Message, Body string Code int } reader := ClientResponseReaderFunc(func(r ClientResponse, _ Consumer) (any, error) { b, _ := io.ReadAll(r.Body()) actual.Body = string(b) actual.Code = r.Code() actual.Message = r.Message() actual.Header = r.GetHeader("blah") return actual, nil }) _, _ = reader.ReadResponse(response{}, nil) assert.EqualT(t, "the content", actual.Body) assert.EqualT(t, "the message", actual.Message) assert.EqualT(t, "the header", actual.Header) assert.EqualT(t, 490, actual.Code) } type errResponse struct { A int `json:"a"` B string `json:"b"` } func TestResponseReaderFuncError(t *testing.T) { t.Parallel() t.Run("with API error as string", func(t *testing.T) { reader := ClientResponseReaderFunc(func(r ClientResponse, _ Consumer) (any, error) { _, _ = io.ReadAll(r.Body()) return nil, NewAPIError("fake", errors.New("writer closed"), 490) }) _, err := reader.ReadResponse(response{}, nil) require.Error(t, err) require.ErrorContains(t, err, "'writer closed'") }) t.Run("with API error as complex error", func(t *testing.T) { reader := ClientResponseReaderFunc(func(r ClientResponse, _ Consumer) (any, error) { _, _ = io.ReadAll(r.Body()) err := &fs.PathError{ Op: "write", Path: "path/to/fake", Err: fs.ErrClosed, } return nil, NewAPIError("fake", err, 200) }) _, err := reader.ReadResponse(response{}, nil) require.Error(t, err) assert.StringContainsT(t, err.Error(), "file already closed") }) t.Run("with API error requiring escaping", func(t *testing.T) { reader := ClientResponseReaderFunc(func(r ClientResponse, _ Consumer) (any, error) { _, _ = io.ReadAll(r.Body()) return nil, NewAPIError("fake", errors.New(`writer is \"terminated\" and 'closed'`), 490) }) _, err := reader.ReadResponse(response{}, nil) require.Error(t, err) require.ErrorContains(t, err, `'writer is \\"terminated\\" and \'closed\''`) }) t.Run("with API error as JSON", func(t *testing.T) { reader := ClientResponseReaderFunc(func(r ClientResponse, _ Consumer) (any, error) { _, _ = io.ReadAll(r.Body()) obj := &errResponse{ // does not implement error A: 555, B: "closed", } return nil, NewAPIError("fake", obj, 200) }) _, err := reader.ReadResponse(response{}, nil) require.Error(t, err) assert.StringContainsT(t, err.Error(), `{"a":555,"b":"closed"}`) }) } go-openapi-runtime-decad8f/constants.go000066400000000000000000000027271520232310000204300ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime const ( // HeaderContentType represents a [http] content-type header, it's value is supposed to be a mime type. HeaderContentType = "Content-Type" // HeaderTransferEncoding represents a [http] transfer-encoding header. HeaderTransferEncoding = "Transfer-Encoding" // HeaderAccept the Accept header. HeaderAccept = "Accept" // HeaderAuthorization the Authorization header. HeaderAuthorization = "Authorization" charsetKey = "charset" // DefaultMime the default fallback mime type. DefaultMime = "application/octet-stream" // JSONMime the json mime type. JSONMime = "application/json" // YAMLMime the [yaml] mime type. Set to the canonical RFC 9512 // name (application/yaml). Legacy forms application/x-yaml, // text/yaml, and text/x-yaml — per RFC 9512 §2.1 "Deprecated // alias names for this type" — resolve to the same codec via // the mediatype alias bridge. YAMLMime = "application/yaml" // XMLMime the [xml] mime type. XMLMime = "application/xml" // TextMime the text mime type. TextMime = "text/plain" // HTMLMime the html mime type. HTMLMime = "text/html" // CSVMime the [csv] mime type. CSVMime = "text/csv" // MultipartFormMime the multipart form mime type. MultipartFormMime = "multipart/form-data" // URLencodedFormMime is the [url] encoded form mime type. URLencodedFormMime = "application/x-www-form-urlencoded" ) go-openapi-runtime-decad8f/consts_test.go000066400000000000000000000005011520232310000207500ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime // Test-only constants shared across the package's *_test.go files, // extracted to satisfy the goconst linter. const ( valValue1 = "value1" valValue2 = "value2" charsetUTF8Val = "utf-8" ) go-openapi-runtime-decad8f/csv.go000066400000000000000000000204021520232310000171750ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "bytes" "context" "encoding" "encoding/csv" "errors" "fmt" "io" "reflect" "golang.org/x/sync/errgroup" ) // CSVConsumer creates a new CSV consumer. // // The consumer consumes CSV records from a provided reader into the data passed by reference. // // CSVOpts options may be specified to alter the default CSV behavior on the reader and the writer side (e.g. separator, skip header, ...). // The defaults are those of the standard library's [csv.Reader] and [csv.Writer]. // // Supported output underlying types and interfaces, prioritized in this order: // // - *[csv.Writer] // - [CSVWriter] (writer options are ignored) // - [io.Writer] (as raw bytes) // - [io.ReaderFrom] (as raw bytes) // - [encoding.BinaryUnmarshaler] (as raw bytes) // - *[][]string (as a collection of records) // - *[]byte (as raw bytes) // - *string (a raw bytes) // // The consumer prioritizes situations where buffering the input is not required. func CSVConsumer(opts ...CSVOpt) Consumer { o := csvOptsWithDefaults(opts) return ConsumerFunc(func(reader io.Reader, data any) error { if reader == nil { return errors.New("CSVConsumer requires a reader") } if data == nil { return errors.New("nil destination for CSVConsumer") } csvReader := csv.NewReader(reader) o.applyToReader(csvReader) closer := defaultCloser if o.closeStream { if cl, isReaderCloser := reader.(io.Closer); isReaderCloser { closer = cl.Close } } defer func() { _ = closer() }() switch destination := data.(type) { case *csv.Writer: csvWriter := destination o.applyToWriter(csvWriter) return pipeCSV(csvWriter, csvReader, o) case CSVWriter: csvWriter := destination // no writer options available return pipeCSV(csvWriter, csvReader, o) case io.Writer: csvWriter := csv.NewWriter(destination) o.applyToWriter(csvWriter) return pipeCSV(csvWriter, csvReader, o) case io.ReaderFrom: var buf bytes.Buffer csvWriter := csv.NewWriter(&buf) o.applyToWriter(csvWriter) if err := bufferedCSV(csvWriter, csvReader, o); err != nil { return err } _, err := destination.ReadFrom(&buf) return err case encoding.BinaryUnmarshaler: var buf bytes.Buffer csvWriter := csv.NewWriter(&buf) o.applyToWriter(csvWriter) if err := bufferedCSV(csvWriter, csvReader, o); err != nil { return err } return destination.UnmarshalBinary(buf.Bytes()) default: // support *[][]string, *[]byte, *string if ptr := reflect.TypeOf(data); ptr.Kind() != reflect.Pointer { return errors.New("destination must be a pointer") } v := reflect.Indirect(reflect.ValueOf(data)) t := v.Type() switch { case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Slice && t.Elem().Elem().Kind() == reflect.String: csvWriter := &csvRecordsWriter{} // writer options are ignored if err := pipeCSV(csvWriter, csvReader, o); err != nil { return err } v.Grow(len(csvWriter.records)) v.SetCap(len(csvWriter.records)) // in case Grow was unnessary, trim down the capacity v.SetLen(len(csvWriter.records)) reflect.Copy(v, reflect.ValueOf(csvWriter.records)) return nil case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8: var buf bytes.Buffer csvWriter := csv.NewWriter(&buf) o.applyToWriter(csvWriter) if err := bufferedCSV(csvWriter, csvReader, o); err != nil { return err } v.SetBytes(buf.Bytes()) return nil case t.Kind() == reflect.String: var buf bytes.Buffer csvWriter := csv.NewWriter(&buf) o.applyToWriter(csvWriter) if err := bufferedCSV(csvWriter, csvReader, o); err != nil { return err } v.SetString(buf.String()) return nil default: return fmt.Errorf("%v (%T) is not supported by the CSVConsumer, %s", data, data, "can be resolved by supporting CSVWriter/Writer/BinaryUnmarshaler interface", ) } } }) } // CSVProducer creates a new CSV producer. // // The producer takes input data then writes as CSV to an output writer (essentially as a pipe). // // Supported input underlying types and interfaces, prioritized in this order: // // - *[csv.Reader] // - [CSVReader] (reader options are ignored) // - [io.Reader] // - [io.WriterTo] // - [encoding.BinaryMarshaler] // - [][]string // - []byte // - string // // The producer prioritizes situations where buffering the input is not required. func CSVProducer(opts ...CSVOpt) Producer { o := csvOptsWithDefaults(opts) return ProducerFunc(func(writer io.Writer, data any) error { if writer == nil { return errors.New("CSVProducer requires a writer") } if data == nil { return errors.New("nil data for CSVProducer") } csvWriter := csv.NewWriter(writer) o.applyToWriter(csvWriter) closer := defaultCloser if o.closeStream { if cl, isWriterCloser := writer.(io.Closer); isWriterCloser { closer = cl.Close } } defer func() { _ = closer() }() if rc, isDataCloser := data.(io.ReadCloser); isDataCloser { defer rc.Close() } switch origin := data.(type) { case *csv.Reader: csvReader := origin o.applyToReader(csvReader) return pipeCSV(csvWriter, csvReader, o) case CSVReader: csvReader := origin // no reader options available return pipeCSV(csvWriter, csvReader, o) case io.Reader: csvReader := csv.NewReader(origin) o.applyToReader(csvReader) return pipeCSV(csvWriter, csvReader, o) case io.WriterTo: // async piping of the writes performed by WriteTo r, w := io.Pipe() csvReader := csv.NewReader(r) o.applyToReader(csvReader) pipe, _ := errgroup.WithContext(context.Background()) pipe.Go(func() error { _, err := origin.WriteTo(w) _ = w.Close() return err }) pipe.Go(func() error { defer func() { _ = r.Close() }() return pipeCSV(csvWriter, csvReader, o) }) return pipe.Wait() case encoding.BinaryMarshaler: buf, err := origin.MarshalBinary() if err != nil { return err } rdr := bytes.NewBuffer(buf) csvReader := csv.NewReader(rdr) return bufferedCSV(csvWriter, csvReader, o) default: // support [][]string, []byte, string (or pointers to those) v := reflect.Indirect(reflect.ValueOf(data)) t := v.Type() switch { case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Slice && t.Elem().Elem().Kind() == reflect.String: csvReader := &csvRecordsWriter{ records: make([][]string, v.Len()), } reflect.Copy(reflect.ValueOf(csvReader.records), v) return pipeCSV(csvWriter, csvReader, o) case t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8: buf := bytes.NewBuffer(v.Bytes()) csvReader := csv.NewReader(buf) o.applyToReader(csvReader) return bufferedCSV(csvWriter, csvReader, o) case t.Kind() == reflect.String: buf := bytes.NewBufferString(v.String()) csvReader := csv.NewReader(buf) o.applyToReader(csvReader) return bufferedCSV(csvWriter, csvReader, o) default: return fmt.Errorf("%v (%T) is not supported by the CSVProducer, %s", data, data, "can be resolved by supporting CSVReader/Reader/BinaryMarshaler interface", ) } } }) } // pipeCSV copies CSV records from a CSV reader to a CSV writer. func pipeCSV(csvWriter CSVWriter, csvReader CSVReader, opts csvOpts) error { for ; opts.skippedLines > 0; opts.skippedLines-- { _, err := csvReader.Read() if err != nil { if errors.Is(err, io.EOF) { return nil } return err } } for { record, err := csvReader.Read() if err != nil { if errors.Is(err, io.EOF) { break } return err } if err := csvWriter.Write(record); err != nil { return err } } csvWriter.Flush() return csvWriter.Error() } // bufferedCSV copies CSV records from a CSV reader to a CSV writer, // by first reading all records then writing them at once. func bufferedCSV(csvWriter *csv.Writer, csvReader *csv.Reader, opts csvOpts) error { for ; opts.skippedLines > 0; opts.skippedLines-- { _, err := csvReader.Read() if err != nil { if errors.Is(err, io.EOF) { return nil } return err } } records, err := csvReader.ReadAll() if err != nil { return err } return csvWriter.WriteAll(records) } go-openapi-runtime-decad8f/csv_options.go000066400000000000000000000045361520232310000207620ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "encoding/csv" "io" ) // CSVOpt alter the behavior of the CSV consumer or producer. type CSVOpt func(*csvOpts) type csvOpts struct { csvReader csv.Reader csvWriter csv.Writer skippedLines int closeStream bool } // WithCSVReaderOpts specifies the options to [csv.Reader] // when reading CSV. func WithCSVReaderOpts(reader csv.Reader) CSVOpt { return func(o *csvOpts) { o.csvReader = reader } } // WithCSVWriterOpts specifies the options to [csv.Writer] // when writing CSV. func WithCSVWriterOpts(writer csv.Writer) CSVOpt { return func(o *csvOpts) { o.csvWriter = writer } } // WithCSVSkipLines will skip header lines. func WithCSVSkipLines(skipped int) CSVOpt { return func(o *csvOpts) { o.skippedLines = skipped } } func WithCSVClosesStream() CSVOpt { return func(o *csvOpts) { o.closeStream = true } } func (o csvOpts) applyToReader(in *csv.Reader) { if o.csvReader.Comma != 0 { in.Comma = o.csvReader.Comma } if o.csvReader.Comment != 0 { in.Comment = o.csvReader.Comment } if o.csvReader.FieldsPerRecord != 0 { in.FieldsPerRecord = o.csvReader.FieldsPerRecord } in.LazyQuotes = o.csvReader.LazyQuotes in.TrimLeadingSpace = o.csvReader.TrimLeadingSpace in.ReuseRecord = o.csvReader.ReuseRecord } func (o csvOpts) applyToWriter(in *csv.Writer) { if o.csvWriter.Comma != 0 { in.Comma = o.csvWriter.Comma } in.UseCRLF = o.csvWriter.UseCRLF } func csvOptsWithDefaults(opts []CSVOpt) csvOpts { var o csvOpts for _, apply := range opts { apply(&o) } return o } type CSVWriter interface { Write([]string) error Flush() Error() error } type CSVReader interface { Read() ([]string, error) } var ( _ CSVWriter = &csvRecordsWriter{} _ CSVReader = &csvRecordsWriter{} ) // csvRecordsWriter is an internal container to move CSV records back and forth. type csvRecordsWriter struct { i int records [][]string } func (w *csvRecordsWriter) Write(record []string) error { w.records = append(w.records, record) return nil } func (w *csvRecordsWriter) Read() ([]string, error) { if w.i >= len(w.records) { return nil, io.EOF } defer func() { w.i++ }() return w.records[w.i], nil } func (w *csvRecordsWriter) Flush() {} func (w *csvRecordsWriter) Error() error { return nil } go-openapi-runtime-decad8f/csv_test.go000066400000000000000000000417241520232310000202460ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "bytes" "encoding/csv" "errors" "io" "net/http/httptest" "strings" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) const ( csvFixture = `name,country,age John,US,19 Mike,US,20 ` badCSVFixture = `name,country,age John,US,19 Mike,US ` commentedCSVFixture = `# heading line name,country,age #John's record John,US,19 #Mike's record Mike,US,20 ` ) var testCSVRecords = [][]string{ {"name", "country", "age"}, {"John", "US", "19"}, {"Mike", "US", "20"}, } func TestCSVConsumer(t *testing.T) { consumer := CSVConsumer() t.Run("can consume as a *csv.Writer", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) var buf bytes.Buffer dest := csv.NewWriter(&buf) err := consumer.Consume(reader, dest) require.NoError(t, err) assert.EqualT(t, csvFixture, buf.String()) }) t.Run("can consume as a CSVReader", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) var dest csvRecordsWriter err := consumer.Consume(reader, &dest) require.NoError(t, err) assertCSVRecords(t, dest.records) }) t.Run("can consume as a Writer", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) var dest closingWriter err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.EqualT(t, csvFixture, dest.b.String()) }) t.Run("can consume as a ReaderFrom", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) var dest readerFromDummy err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.EqualT(t, csvFixture, dest.b.String()) }) t.Run("can consume as a BinaryUnmarshaler", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) var dest binaryUnmarshalDummy err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.EqualT(t, csvFixture, dest.str) }) t.Run("can consume as a *[][]string", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) dest := [][]string{} err := consumer.Consume(reader, &dest) require.NoError(t, err) assertCSVRecords(t, dest) }) t.Run("can consume as an alias to *[][]string", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) type records [][]string var dest records err := consumer.Consume(reader, &dest) require.NoError(t, err) assertCSVRecords(t, dest) }) t.Run("can consume as a *[]byte", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) var dest []byte err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.EqualT(t, csvFixture, string(dest)) }) t.Run("can consume as an alias to *[]byte", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) type buffer []byte var dest buffer err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.EqualT(t, csvFixture, string(dest)) }) t.Run("can consume as a *string", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) var dest string err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.EqualT(t, csvFixture, dest) }) t.Run("can consume as an alias to *string", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) type buffer string var dest buffer err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.EqualT(t, csvFixture, string(dest)) }) t.Run("can consume from an empty reader", func(t *testing.T) { reader := &csvEmptyReader{} var dest bytes.Buffer err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.Empty(t, dest.String()) }) t.Run("error cases", func(t *testing.T) { t.Run("nil data is never accepted", func(t *testing.T) { var rdr bytes.Buffer require.Error(t, consumer.Consume(&rdr, nil)) }) t.Run("nil readers should also never be acccepted", func(t *testing.T) { var buf bytes.Buffer err := consumer.Consume(nil, &buf) require.Error(t, err) }) t.Run("data must be a pointer", func(t *testing.T) { var rdr bytes.Buffer var dest []byte err := consumer.Consume(&rdr, dest) require.Error(t, err) }) t.Run("unsupported type", func(t *testing.T) { var rdr bytes.Buffer var dest struct{} err := consumer.Consume(&rdr, &dest) require.Error(t, err) }) t.Run("should propagate CSV error (buffered)", func(t *testing.T) { reader := bytes.NewBufferString(badCSVFixture) var dest []byte err := consumer.Consume(reader, &dest) require.Error(t, err) require.EqualError(t, err, "record on line 3: wrong number of fields") }) t.Run("should propagate CSV error (buffered, string)", func(t *testing.T) { reader := bytes.NewBufferString(badCSVFixture) var dest string err := consumer.Consume(reader, &dest) require.Error(t, err) require.EqualError(t, err, "record on line 3: wrong number of fields") }) t.Run("should propagate CSV error (buffered, ReaderFrom)", func(t *testing.T) { reader := bytes.NewBufferString(badCSVFixture) var dest readerFromDummy err := consumer.Consume(reader, &dest) require.Error(t, err) require.EqualError(t, err, "record on line 3: wrong number of fields") }) t.Run("should propagate CSV error (buffered, BinaryUnmarshaler)", func(t *testing.T) { reader := bytes.NewBufferString(badCSVFixture) var dest binaryUnmarshalDummy err := consumer.Consume(reader, &dest) require.Error(t, err) require.EqualError(t, err, "record on line 3: wrong number of fields") }) t.Run("should propagate CSV error (streaming)", func(t *testing.T) { reader := bytes.NewBufferString(badCSVFixture) var dest bytes.Buffer err := consumer.Consume(reader, &dest) require.Error(t, err) require.EqualError(t, err, "record on line 3: wrong number of fields") }) t.Run("should propagate CSV error (streaming, write error)", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) var buf bytes.Buffer dest := csvWriterDummy{err: errors.New("test error"), Writer: csv.NewWriter(&buf)} err := consumer.Consume(reader, &dest) require.Error(t, err) require.EqualError(t, err, "test error") }) t.Run("should propagate ReaderFrom error", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) dest := readerFromDummy{err: errors.New("test error")} err := consumer.Consume(reader, &dest) require.Error(t, err) require.EqualError(t, err, "test error") }) t.Run("should propagate BinaryUnmarshaler error", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) dest := binaryUnmarshalDummy{err: errors.New("test error")} err := consumer.Consume(reader, &dest) require.Error(t, err) require.EqualError(t, err, "test error") }) }) } func TestCSVConsumerWithOptions(t *testing.T) { semiColonFixture := strings.ReplaceAll(csvFixture, ",", ";") t.Run("with CSV reader Comma", func(t *testing.T) { consumer := CSVConsumer(WithCSVReaderOpts(csv.Reader{Comma: ';', FieldsPerRecord: 3})) t.Run("should not read comma-separated input", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) var dest bytes.Buffer err := consumer.Consume(reader, &dest) require.Error(t, err) require.EqualError(t, err, "record on line 1: wrong number of fields") }) t.Run("should read semicolon-separated input and convert it to colon-separated", func(t *testing.T) { reader := bytes.NewBufferString(semiColonFixture) var dest bytes.Buffer err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.EqualT(t, csvFixture, dest.String()) }) }) t.Run("with CSV reader Comment", func(t *testing.T) { consumer := CSVConsumer(WithCSVReaderOpts(csv.Reader{Comment: '#'})) t.Run("should read input and skip commented lines", func(t *testing.T) { reader := bytes.NewBufferString(commentedCSVFixture) var dest [][]string err := consumer.Consume(reader, &dest) require.NoError(t, err) assertCSVRecords(t, dest) }) }) t.Run("with CSV writer Comma", func(t *testing.T) { consumer := CSVConsumer(WithCSVWriterOpts(csv.Writer{Comma: ';'})) t.Run("should read comma-separated input and convert it to semicolon-separated", func(t *testing.T) { reader := bytes.NewBufferString(csvFixture) var dest bytes.Buffer err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.EqualT(t, semiColonFixture, dest.String()) }) }) t.Run("with SkipLines (streaming)", func(t *testing.T) { consumer := CSVConsumer(WithCSVSkipLines(1)) reader := bytes.NewBufferString(csvFixture) var dest [][]string err := consumer.Consume(reader, &dest) require.NoError(t, err) expected := testCSVRecords[1:] assert.Equalf(t, expected, dest, "expected output to skip header") }) t.Run("with SkipLines (buffered)", func(t *testing.T) { consumer := CSVConsumer(WithCSVSkipLines(1)) reader := bytes.NewBufferString(csvFixture) var dest []byte err := consumer.Consume(reader, &dest) require.NoError(t, err) r := csv.NewReader(bytes.NewReader(dest)) consumed, err := r.ReadAll() require.NoError(t, err) expected := testCSVRecords[1:] assert.Equalf(t, expected, consumed, "expected output to skip header") }) t.Run("should detect errors on skipped lines (streaming)", func(t *testing.T) { consumer := CSVConsumer(WithCSVSkipLines(1)) reader := bytes.NewBufferString(strings.ReplaceAll(csvFixture, ",age", `,"age`)) var dest [][]string err := consumer.Consume(reader, &dest) require.Error(t, err) require.ErrorContains(t, err, "record on line 1; parse error") }) t.Run("should detect errors on skipped lines (buffered)", func(t *testing.T) { consumer := CSVConsumer(WithCSVSkipLines(1)) reader := bytes.NewBufferString(strings.ReplaceAll(csvFixture, ",age", `,"age`)) var dest []byte err := consumer.Consume(reader, &dest) require.Error(t, err) require.ErrorContains(t, err, "record on line 1; parse error") }) t.Run("with SkipLines greater than the total number of lines (streaming)", func(t *testing.T) { consumer := CSVConsumer(WithCSVSkipLines(4)) reader := bytes.NewBufferString(csvFixture) var dest [][]string err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.Empty(t, dest) }) t.Run("with SkipLines greater than the total number of lines (buffered)", func(t *testing.T) { consumer := CSVConsumer(WithCSVSkipLines(4)) reader := bytes.NewBufferString(csvFixture) var dest []byte err := consumer.Consume(reader, &dest) require.NoError(t, err) assert.Empty(t, dest) }) t.Run("with CloseStream", func(t *testing.T) { t.Run("wants to close stream", func(t *testing.T) { closingConsumer := CSVConsumer(WithCSVClosesStream()) var dest bytes.Buffer r := &closingReader{b: bytes.NewBufferString(csvFixture)} require.NoError(t, closingConsumer.Consume(r, &dest)) assert.EqualT(t, csvFixture, dest.String()) assert.EqualValues(t, 1, r.calledClose) }) t.Run("don't want to close stream", func(t *testing.T) { nonClosingConsumer := CSVConsumer() var dest bytes.Buffer r := &closingReader{b: bytes.NewBufferString(csvFixture)} require.NoError(t, nonClosingConsumer.Consume(r, &dest)) assert.EqualT(t, csvFixture, dest.String()) assert.EqualValues(t, 0, r.calledClose) }) }) } func TestCSVProducer(t *testing.T) { producer := CSVProducer() t.Run("can produce CSV from *csv.Reader", func(t *testing.T) { writer := new(bytes.Buffer) buf := bytes.NewBufferString(csvFixture) data := csv.NewReader(buf) err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.String()) }) t.Run("can produce CSV from CSVReader", func(t *testing.T) { writer := new(bytes.Buffer) data := &csvRecordsWriter{ records: testCSVRecords, } err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.String()) }) t.Run("can produce CSV from Reader", func(t *testing.T) { writer := new(bytes.Buffer) data := bytes.NewReader([]byte(csvFixture)) err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.String()) }) t.Run("can produce CSV from WriterTo", func(t *testing.T) { writer := new(bytes.Buffer) buf := bytes.NewBufferString(csvFixture) data := &writerToDummy{ b: *buf, } err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.String()) }) t.Run("can produce CSV from BinaryMarshaler", func(t *testing.T) { writer := new(bytes.Buffer) data := &binaryMarshalDummy{str: csvFixture} err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.String()) }) t.Run("can produce CSV from [][]string", func(t *testing.T) { writer := new(bytes.Buffer) data := testCSVRecords err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.String()) }) t.Run("can produce CSV from alias to [][]string", func(t *testing.T) { writer := new(bytes.Buffer) type records [][]string data := records(testCSVRecords) err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.String()) }) t.Run("can produce CSV from []byte", func(t *testing.T) { writer := httptest.NewRecorder() data := []byte(csvFixture) err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.Body.String()) }) t.Run("can produce CSV from alias to []byte", func(t *testing.T) { writer := httptest.NewRecorder() type buffer []byte data := buffer(csvFixture) err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.Body.String()) }) t.Run("can produce CSV from string", func(t *testing.T) { writer := httptest.NewRecorder() data := csvFixture err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.Body.String()) }) t.Run("can produce CSV from alias to string", func(t *testing.T) { writer := httptest.NewRecorder() type buffer string data := buffer(csvFixture) err := producer.Produce(writer, data) require.NoError(t, err) assert.EqualT(t, csvFixture, writer.Body.String()) }) t.Run("always close data reader whenever possible", func(t *testing.T) { nonClosingProducer := CSVProducer() r := &closingWriter{} data := &closingReader{b: bytes.NewBufferString(csvFixture)} require.NoError(t, nonClosingProducer.Produce(r, data)) assert.EqualT(t, csvFixture, r.String()) assert.EqualValuesf(t, 0, r.calledClose, "expected the input reader NOT to be closed") assert.EqualValuesf(t, 1, data.calledClose, "expected the data reader to be closed") }) t.Run("error cases", func(t *testing.T) { t.Run("unsupported type", func(t *testing.T) { writer := httptest.NewRecorder() var data struct{} err := producer.Produce(writer, data) require.Error(t, err) }) t.Run("data cannot be nil", func(t *testing.T) { writer := httptest.NewRecorder() err := producer.Produce(writer, nil) require.Error(t, err) }) t.Run("writer cannot be nil", func(t *testing.T) { data := []byte(csvFixture) err := producer.Produce(nil, data) require.Error(t, err) }) t.Run("should propagate error from BinaryMarshaler", func(t *testing.T) { var rdr bytes.Buffer data := new(binaryMarshalDummy) err := producer.Produce(&rdr, data) require.Error(t, err) require.ErrorContains(t, err, "no text set") }) }) } func TestCSVProducerWithOptions(t *testing.T) { t.Run("with CloseStream", func(t *testing.T) { t.Run("wants to close stream", func(t *testing.T) { closingProducer := CSVProducer(WithCSVClosesStream()) r := &closingWriter{} data := bytes.NewBufferString(csvFixture) require.NoError(t, closingProducer.Produce(r, data)) assert.EqualT(t, csvFixture, r.String()) assert.EqualValues(t, 1, r.calledClose) }) t.Run("don't want to close stream", func(t *testing.T) { nonClosingProducer := CSVProducer() r := &closingWriter{} data := bytes.NewBufferString(csvFixture) require.NoError(t, nonClosingProducer.Produce(r, data)) assert.EqualT(t, csvFixture, r.String()) assert.EqualValues(t, 0, r.calledClose) }) }) } func assertCSVRecords(t testing.TB, dest [][]string) { assert.Len(t, dest, 3) for i, record := range dest { assert.Equal(t, testCSVRecords[i], record) } } type csvEmptyReader struct{} func (r *csvEmptyReader) Read(_ []byte) (int, error) { return 0, io.EOF } type readerFromDummy struct { err error b bytes.Buffer } func (r *readerFromDummy) ReadFrom(rdr io.Reader) (int64, error) { if r.err != nil { return 0, r.err } return r.b.ReadFrom(rdr) } type writerToDummy struct { b bytes.Buffer } func (w *writerToDummy) WriteTo(writer io.Writer) (int64, error) { return w.b.WriteTo(writer) } type csvWriterDummy struct { *csv.Writer err error } func (w *csvWriterDummy) Write(record []string) error { if w.err != nil { return w.err } return w.Writer.Write(record) } func (w *csvWriterDummy) Error() error { if w.err != nil { return w.err } return w.Writer.Error() } go-openapi-runtime-decad8f/discard.go000066400000000000000000000006571520232310000200250ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import "io" // DiscardConsumer does absolutely nothing, it's a black hole. var DiscardConsumer = ConsumerFunc(func(_ io.Reader, _ any) error { return nil }) // DiscardProducer does absolutely nothing, it's a black hole. var DiscardProducer = ProducerFunc(func(_ io.Writer, _ any) error { return nil }) go-openapi-runtime-decad8f/doc.go000066400000000000000000000003311520232310000171460ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // Package runtime exposes runtime client and server components // for go-openapi toolkit. package runtime go-openapi-runtime-decad8f/docs/000077500000000000000000000000001520232310000170055ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/NOTES.md000066400000000000000000000015541520232310000202240ustar00rootroot00000000000000### v0.29.0 **New with this release**: * upgraded to `go1.24` and modernized the code base accordingly * updated all dependencies, and removed an noticeable indirect dependency (e.g. `mailru/easyjson`) * **breaking change** no longer imports `opentracing-go` (#365). * the `WithOpentracing()` method now returns an opentelemetry transport * for users who can't transition to opentelemetry, the previous behavior of `WithOpentracing` delivering an opentracing transport is provided by a separate module `github.com/go-openapi/runtime/client-middleware/opentracing`. * removed direct dependency to `gopkg.in/yaml.v3`, in favor of `go.yaml.in/yaml/v3` (an indirect test dependency to the older package is still around) * technically, the repo has evolved to a mono-repo, multiple modules structures (2 go modules published), with CI adapted accordingly go-openapi-runtime-decad8f/docs/doc-site/000077500000000000000000000000001520232310000205145ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/_index.md000066400000000000000000000061041520232310000223050ustar00rootroot00000000000000--- title: "go-openapi runtime" type: home description: HTTP runtime for OpenAPI clients and servers in Go. weight: 1 --- `github.com/go-openapi/runtime` is a runtime library used to work with OpenAPI. At this moment, it only supports OpenAPI v2 (aka Swagger). It is used by clients and servers generated with [go-swagger][go-swagger]. or directly by applications that build untyped OpenAPI / Swagger clients or servers. It ships: * a configurable HTTP **client transport** (`client.Runtime`) — TLS, proxy, timeouts, OpenTelemetry tracing, pluggable authentication * a **server middleware pipeline** that turns an analyzed OpenAPI spec into a working `http.Handler` — routing, security, parameter binding, validation and operation execution * a **dependency-free server-middleware module** with media-type processing, content negotiation and doc-UI helpers, usable from any plain `net/http` server ### Status {{% button href="https://github.com/go-openapi/runtime/fork" hint="fork me on github" style=primary icon=code-fork %}}Fork me{{% /button %}} Stable API. Actively maintained. ### Getting started ```cmd go get github.com/go-openapi/runtime ``` Using only the dependency-free middleware (media types, negotiation, doc UIs): ```cmd go get github.com/go-openapi/runtime/server-middleware ``` ### Where to go next {{< cards >}} {{% card title="Features" %}} Features supported by our client and server, with normative references. → [usage/features](./usage/features/) {{% /card %}} {{% card title="Core" %}} The five interfaces (`Consumer`, `Producer`, `Authenticator`, `Authorizer`, `OperationHandler`) every other piece is built on, plus content-type and validation plumbing. → [usage/core](./usage/core/) {{% /card %}} {{% card title="Client" %}} Configuring `client.Runtime` for TLS, auth, OpenTelemetry tracing and context-aware request submission. → [usage/client](./usage/client/) {{% /card %}} {{% card title="Server" %}} The Router → Binder → Validator → Security → OperationExecutor → Responder pipeline that turns a spec into a handler. → [usage/server](./usage/server/) {{% /card %}} {{% card title="Standalone" %}} Use the media-type, content-negotiation and doc-UI helpers from any plain `net/http` server, with no transitive OpenAPI dependencies. → [usage/standalone](./usage/standalone/) {{% /card %}} {{< /cards >}} Looking for runnable code? See [examples](./usage/examples/). ## Licensing `SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers` This library ships under the [Apache-2.0 license](./project/LICENSE.md). ## Contributing Issues and pull requests welcome. See the shared [go-openapi contributing guidelines][contributing-doc-site] and the per-repo notes in [project/](./project/). --- {{< children type="card" description="true" >}} [go-swagger]: https://github.com/go-swagger/go-swagger [contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html [maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html go-openapi-runtime-decad8f/docs/doc-site/project/000077500000000000000000000000001520232310000221625ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/project/LICENSE.md000066400000000000000000000262471520232310000236010ustar00rootroot00000000000000--- title: LICENSE description: Apache-2.0 License weight: 10 --- ``` Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` go-openapi-runtime-decad8f/docs/doc-site/project/README.md000066400000000000000000000136521520232310000234500ustar00rootroot00000000000000# runtime --- A runtime for go OpenAPI toolkit. The runtime component for use in code generation or as untyped usage. ## Announcements **Changes to the API surface in `v0.30.0`**: * utility package `header` has now moved to `github.com/go-openapi/runtime/server-middleware/negotiate/header` > A shim is provided to support existing programs, with a deprecation notice. **Changes in semantics in `v0.30.0`**: Function `negotiate.NegotiateContentType` (available as an alias for backward compatibility as `middleware.NegotiateContentType` now performs a full match considering MIME parameters. The previous behavior (matching in order of appearance after stripping parameters) may be enabled explicitly with option `negotiate.WithIgnoreParameters`. * **2026-05-05** : exposed content negotiation methods as a separate, dependency-free module > Users may reuse these utilities to support content-negotiation without extra dependencies. > > Newly available module: `github.com/go-openapi/runtime/server-middleware` > > Newly available packages: `github.com/go-openapi/runtime/server-middleware/negotiate` and > `github.com/go-openapi/runtime/server-middleware/mediatype`. * **2026-05-07** : exposed UI and Spec middleware as a separate, dependency-free module. > Newly available package: `github.com/go-openapi/runtime/server-middleware/docui` that now holds our > UI and spec serve middleware. > > A shim is available in `github.com/go-openapi/runtime/middleware` to bridge the older UI options to the new ones, > with a deprecation notice. > > Methods that were unduly exported and purely used to manipulate options (e.g. `SwaggerUIOpts.EnsureDefaults`) have been > removed. New options in `docui` should be used instead. > Users may reuse this middleware to serve a Redoc, Rapidoc or SwaggerUI documentation without > importing the complete go-openapi scaffolding. ## Status API is stable. ## Import this library in your project ```cmd go get github.com/go-openapi/runtime ``` ## Change log See For v0.29.0 release see [release notes](docs/NOTES.md). From that release onwards, changes are tracked in the github release notes. **What coming next?** Moving forward, we want to : * [x] fix a few known issues with some file upload requests (e.g. #286) * [] continue narrowing down the scope of dependencies: * [x] split middleware and other useful utilities as a separate dependency-free module * yaml support in an independent module (v2) * introduce more up-to-date support for opentelemetry as a separate module that evolves independently from the main package (to avoid breaking changes, the existing API will remain maintained, but evolve at a slower pace than opentelemetry). (v2) * [] publish proper documentation and examples ## Licensing This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE). See the license [NOTICE](./NOTICE), which recalls the licensing terms of all the pieces of software on top of which it has been built. ## Other documentation * [FAQ](../../tutorials/faq/) · [Media-type selection](../../tutorials/media-types/) · [Client keep-alive](../../tutorials/keep-alive/) * [All-time contributors](./CONTRIBUTORS.md) * [Contributing guidelines][contributing-doc-site] * [Maintainers documentation][maintainers-doc-site] * [Code style][style-doc-site] ## Cutting a new release Maintainers can cut a new release by either: * running [this workflow](https://github.com/go-openapi/runtime/actions/workflows/bump-release.yml) * or pushing a semver tag * signed tags are preferred * The tag message is prepended to release notes [test-badge]: https://github.com/go-openapi/runtime/actions/workflows/go-test.yml/badge.svg [test-url]: https://github.com/go-openapi/runtime/actions/workflows/go-test.yml [cov-badge]: https://codecov.io/gh/go-openapi/runtime/branch/master/graph/badge.svg [cov-url]: https://codecov.io/gh/go-openapi/runtime [vuln-scan-badge]: https://github.com/go-openapi/runtime/actions/workflows/scanner.yml/badge.svg [vuln-scan-url]: https://github.com/go-openapi/runtime/actions/workflows/scanner.yml [codeql-badge]: https://github.com/go-openapi/runtime/actions/workflows/codeql.yml/badge.svg [codeql-url]: https://github.com/go-openapi/runtime/actions/workflows/codeql.yml [release-badge]: https://badge.fury.io/gh/go-openapi%2Fruntime.svg [release-url]: https://badge.fury.io/gh/go-openapi%2Fruntime [gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/runtime [gocard-url]: https://goreportcard.com/report/github.com/go-openapi/runtime [codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/runtime [codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/runtime [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/runtime [godoc-url]: http://pkg.go.dev/github.com/go-openapi/runtime [discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue [discord-url]: https://discord.gg/FfnFYaC3k5 [license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg [license-url]: https://github.com/go-openapi/runtime/?tab=Apache-2.0-1-ov-file#readme [goversion-badge]: https://img.shields.io/github/go-mod/go-version/go-openapi/runtime [goversion-url]: https://github.com/go-openapi/runtime/blob/master/go.mod [top-badge]: https://img.shields.io/github/languages/top/go-openapi/runtime [commits-badge]: https://img.shields.io/github/commits-since/go-openapi/runtime/latest [contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html [maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html [style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html go-openapi-runtime-decad8f/docs/doc-site/project/_index.md000066400000000000000000000015271520232310000237570ustar00rootroot00000000000000--- title: "Project" weight: 4 description: | Repo-level information for github.com/go-openapi/runtime. Cross-org contributing and maintainer guides live in the shared go-openapi doc-site. --- This section holds material specific to this repository: * [README](./readme/) — repo overview and announcements * [License](./license/) — Apache-2.0 Cross-org documentation that applies to every go-openapi repo lives in the shared doc-site: * [Contributing guidelines][contributing-doc-site] * [Maintainers documentation][maintainers-doc-site] * [Code style][style-doc-site] [contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html [maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html [style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html go-openapi-runtime-decad8f/docs/doc-site/tutorials/000077500000000000000000000000001520232310000225425ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/tutorials/_index.md000066400000000000000000000010171520232310000243310ustar00rootroot00000000000000--- title: Tutorials weight: 3 description: | Educational deep-dives and FAQs covering both simple and advanced usage of the runtime. Start with the FAQ for quick answers; jump to a topic page for the full algorithm or behaviour reference. --- The pages in this section are *reference-quality* explanations, not recipes. They live alongside the [Usage](../usage/) pages but go deeper — when a 415 surprises you, or a quiet connection starts returning `context deadline exceeded`, this is where the explanation lives. go-openapi-runtime-decad8f/docs/doc-site/tutorials/faq.md000066400000000000000000000127631520232310000236440ustar00rootroot00000000000000--- title: FAQ weight: 10 description: | Frequently asked questions collected from GitHub issues — quick answers on TLS, debugging, content types, and common gotchas. --- Answers to common questions collected from [GitHub issues](https://github.com/go-openapi/runtime/issues). --- ## Client ### How do I disable TLS certificate verification? Use `TLSClientOptions` with `InsecureSkipVerify`: ```go import "github.com/go-openapi/runtime/client" httpClient, err := client.TLSClient(client.TLSClientOptions{ InsecureSkipVerify: true, }) ``` Then pass the resulting `*http.Client` to your transport. > [#196](https://github.com/go-openapi/runtime/issues/196) ### Why is `request.ContentLength` zero when I send a body? A streaming body (e.g. from `bytes.NewReader`) is sent with chunked transfer encoding. The runtime cannot know the content length of an arbitrary stream unless you explicitly set it on the request. If you need `ContentLength` populated, set it yourself before submitting. > [#253](https://github.com/go-openapi/runtime/issues/253) ### How do I read the error response body from an `APIError`? The client's `Submit()` closes the response body after reading. To access error details, define your error responses (including a `default` response) in the Swagger spec with a schema. The generated client will then deserialize the error body into a typed struct that you can access via type assertion: ```go if apiErr, ok := err.(*mypackage.GetThingDefault); ok { // apiErr.Payload contains the deserialized error body } ``` Without a response schema in the spec, the body is discarded and only the status code is available in the `runtime.APIError`. > [#89](https://github.com/go-openapi/runtime/issues/89), [#121](https://github.com/go-openapi/runtime/issues/121) ### How do I register custom MIME types (e.g. `application/problem+json`)? The default client runtime ships with a fixed set of consumers/producers. Register custom ones on the transport: ```go rt := client.New(host, basePath, schemes) rt.Consumers["application/problem+json"] = runtime.JSONConsumer() rt.Producers["application/problem+json"] = runtime.JSONProducer() ``` The same approach works for any non-standard MIME type such as `application/pdf` (use `runtime.ByteStreamConsumer()`), `application/hal+json`, or `application/vnd.error+json` (use `runtime.JSONConsumer()`). > [#31](https://github.com/go-openapi/runtime/issues/31), [#252](https://github.com/go-openapi/runtime/issues/252), [#329](https://github.com/go-openapi/runtime/issues/329) --- ## Middleware ### How do I access the authenticated Principal from an `OperationHandler`? Use the context helpers from the `middleware` package: ```go func myHandler(r *http.Request, params MyParams) middleware.Responder { principal := middleware.SecurityPrincipalFrom(r) route := middleware.MatchedRouteFrom(r) scopes := middleware.SecurityScopesFrom(r) // ... } ``` These extract values that the middleware pipeline stored in the request context during authentication and routing. > [#203](https://github.com/go-openapi/runtime/issues/203) ### Can I run authentication on requests that don't match a route? No. Authentication is determined dynamically per route from the OpenAPI spec (each operation declares its own security requirements). The middleware pipeline authenticates *after* routing, so unmatched requests are never authenticated. > [#201](https://github.com/go-openapi/runtime/issues/201) ### How do I share context values across middlewares when using an external router? The go-openapi router creates a new request context during route resolution. Context values set after routing (e.g. during auth) are not visible to middlewares that run before the router in the chain. The recommended pattern is to use a pointer-based shared struct: ```go type sharedCtx struct { Principal any // add fields as needed } // In your outermost middleware, before the router: sc := &sharedCtx{} ctx := context.WithValue(r.Context(), sharedCtxKey, sc) next.ServeHTTP(w, r.WithContext(ctx)) // After ServeHTTP returns, sc is populated by inner middlewares. // In an inner middleware or auth handler: sc := r.Context().Value(sharedCtxKey).(*sharedCtx) sc.Principal = principal // visible to the outer middleware ``` Because the struct is shared by pointer, mutations are visible regardless of which request copy carries the context. > [#375](https://github.com/go-openapi/runtime/issues/375) ### Can I use this library to validate requests/responses without code generation? Yes. Use the routing and validation middleware from the `middleware` package with an untyped API. Load your spec with `loads.Spec()`, then wire up `middleware.NewRouter()` to get request validation against the spec without needing go-swagger generated code. See the `middleware/untyped` package for examples. > [#44](https://github.com/go-openapi/runtime/issues/44) ### How do I configure Swagger UI to show multiple specs? `SwaggerUIOpts` supports the `urls` parameter for listing multiple spec files in the Swagger UI explore bar. Configure it instead of the single `url` parameter. > [#316](https://github.com/go-openapi/runtime/issues/316) --- ## Documentation ### Where can I find middleware documentation? - [GoDoc](https://pkg.go.dev/github.com/go-openapi/runtime/middleware) — API reference - [go-swagger middleware guide](https://goswagger.io/use/middleware.html) — usage patterns - [go-swagger FAQ](https://goswagger.io/faq/) — common questions > [#82](https://github.com/go-openapi/runtime/issues/82) go-openapi-runtime-decad8f/docs/doc-site/tutorials/keep-alive.md000066400000000000000000000403321520232310000251100ustar00rootroot00000000000000--- title: Keep-alive in the runtime client weight: 30 description: | How `go-openapi/runtime` reuses TCP connections, what the kernel and the HTTP transport actually do for you, and where it goes wrong when there is a NAT gateway, proxy, or firewall between your client and the server. --- How `go-openapi/runtime` reuses TCP connections, what the kernel and the HTTP transport actually do for you, and where it goes wrong when there is a NAT gateway, proxy, or firewall between your client and the server. Concrete: the reference for "I get `context deadline exceeded` after a quiet period" — issue #336 is the canonical example. > Scope: client-side `Runtime`. Server-side keep-alive (`http.Server`'s > own timers) is summarised briefly at the end, with pointers into the > Go stdlib docs. ## TL;DR If your client lives behind a NAT gateway, a load balancer, or a firewall with an idle conntrack timeout (AWS NAT: **350 seconds**; many corporate firewalls: a few minutes), and you see `context deadline exceeded` on requests that follow a quiet period: 1. **Check what your `Runtime.Transport` is.** If you let `client.New` pick the default (`http.DefaultTransport`), you already get `IdleConnTimeout = 90s` and `Dialer.KeepAlive = 30s`. Those defeat most NAT timeouts. 2. **If you replaced the Transport** (for TLS config, a proxy, etc.), you almost certainly lost those defaults. Reinstate them. 3. **On Go 1.23+, set an explicit [`net.Dialer.KeepAliveConfig`](https://pkg.go.dev/net#Dialer)** — the bare `KeepAlive` field only sets the probe *interval*, not the *idle delay before probing starts*. On Linux the kernel default for the idle delay is often **7200 seconds** (two hours), so probes never fire before a 350s NAT timeout drops your conntrack. 4. **Do not** reach for [`Runtime.EnableConnectionReuse`](https://pkg.go.dev/github.com/go-openapi/runtime/client#Runtime.EnableConnectionReuse). The name is misleading — it does not control TCP keepalive or NAT timeouts. See "the misnomer" below. A recipe at the bottom of this document covers the cloud / NAT case. ## Two distinct things named "keep-alive" The word "keep-alive" is used for two unrelated mechanisms operating at different layers. The runtime, the stdlib, and the OS all speak about "keep-alive" without always disambiguating, which is the root of most confusion. ### HTTP keep-alive (application layer) `Connection: keep-alive` is an HTTP/1.1 default. It means **the same TCP connection serves multiple HTTP request/response pairs**. The client sends request 1, reads response 1, sends request 2 *on the same socket*, reads response 2, and so on, until either side closes. In Go, `http.Transport` keeps a pool of idle connections per host. After a response body is fully read and closed, the connection goes back to the pool. The next request to the same host may pick a connection from the pool instead of dialling a new one. Skipping the dial saves a TCP handshake plus, for HTTPS, a TLS handshake — typically tens to hundreds of milliseconds per request. ### TCP keepalive (kernel / socket layer) `SO_KEEPALIVE` is a socket option asking the kernel to **send periodic empty ACK packets** on an otherwise-idle TCP connection. The peer acknowledges them. Two consequences: 1. **Dead-peer detection.** If the peer disappears (machine rebooted, network partitioned), the kernel sees the missing ACKs and tears the connection down. Without keepalive, a half-open connection can linger indefinitely. 2. **Conntrack / NAT keep-alive.** A NAT gateway or stateful firewall maintains a *connection tracking* (conntrack) entry per TCP flow passing through it. The entry is dropped after some idle period — AWS NAT uses 350 seconds, many enterprise firewalls use 60s–15min. Once the entry is dropped, packets arriving for that flow are either silently discarded or rejected with a RST that may not reach the original sender. **Periodic TCP keepalive packets count as live traffic**, so the NAT keeps the entry fresh. The first three of the four mechanisms below are HTTP keep-alive concerns; the fourth is the kernel/TCP one. | Knob | Layer | Default | What it controls | |---|---|---|---| | `http.Transport.DisableKeepAlives` | HTTP | `false` (keep-alive on) | Whether a TCP conn serves more than one HTTP request | | `http.Transport.MaxIdleConns` / `MaxIdleConnsPerHost` | HTTP | 100 / 2 | Idle-pool sizing | | `http.Transport.IdleConnTimeout` | HTTP | 90s | How long an idle conn stays in the pool before close | | `net.Dialer.KeepAlive` (+ `KeepAliveConfig` on Go 1.23+) | TCP | 30s | Whether and how the kernel sends keepalive probes on dialled connections | If you only remember one thing: **HTTP-layer settings decide whether the runtime *reuses* a connection. TCP-layer settings decide whether a through-the-network proxy *still believes the connection exists*.** A mismatch produces issue #336. ## What Go does for you, by default `http.DefaultTransport` is the transport [`client.New`](https://pkg.go.dev/github.com/go-openapi/runtime/client#New) sets on every fresh `Runtime`. Its defaults, as of recent Go: ```go // from net/http DefaultTransport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, // TCP keepalive interval }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } ``` Read the way most cloud-deployed Go services need it: - `IdleConnTimeout = 90s` is less than the AWS NAT 350s timeout, so an idle pooled connection is closed by Go before NAT drops it. - `Dialer.KeepAlive = 30s` enables TCP keepalive probes every 30s, so *active* connections survive long NAT timeouts even when the application isn't sending data. For typical cases, these defaults are correct. **You only need to think about this if you replaced the Transport, or if your environment has an unusual idle timeout.** ## How the runtime wires this `Runtime.Transport` is the `http.RoundTripper` used for every outbound request. Three things to know: 1. The default is `http.DefaultTransport`, with the values above. 2. Replacing `rt.Transport = ...` with a custom transport **completely overrides** the defaults — you inherit nothing unless you copy what `http.DefaultTransport` sets. 3. `Runtime.SetDebug(true)` does not affect keep-alive at all — it only logs requests/responses. ### The misnomer — `Runtime.EnableConnectionReuse` `Runtime.EnableConnectionReuse()` is the method most users find when searching for "keep-alive" or "connection reuse" in this codebase. The name suggests it controls whether connections are pooled and reused. It does not. What it actually does: wraps `Runtime.Transport` in a `RoundTripper` that, after every response, **drains any unread bytes from the response body** before `Close`. The reason: Go's `http.Transport` will only return a connection to the idle pool if the response body was fully read. If your handler stops reading early — for example, you only need the HTTP status and skip the body — the connection is not reusable, and the next request will pay the cost of a new dial + handshake. So `EnableConnectionReuse` is a narrow fix for one specific pattern: code that doesn't fully read response bodies. It has **no effect on**: - TCP keepalive packets; - whether the connection survives a NAT idle timeout; - the size of the idle pool; - the idle timeout in the pool; - any other connection-lifecycle concern. If you ended up here following the issue #336 trail: this method will not help you. A future runtime release will either rename this method to something narrow and honest, or fold the body-draining behaviour into a default-on path so users no longer have to know about it. ## The NAT idle-timeout failure mode This is the scenario in issue #336. Walk through it once and the symptom becomes recognisable: 1. The client makes a request. Go dials a fresh TCP connection through the NAT gateway. NAT creates a conntrack entry. Request completes; the connection goes into Go's idle pool. 2. The application is quiet for **more than 350 seconds**. 3. Go's idle pool has not yet evicted the connection (if you increased `IdleConnTimeout` past 350s, or if you have a custom transport that doesn't set it). Or the conn is "active" because something is waiting on it, just not sending data — long polling, server-sent events, slow streaming response. 4. **NAT drops the conntrack entry.** No notification to either side. 5. The application makes its next request. Go picks the still-pooled connection. The TCP stack believes it is fine; it sends. 6. **Packets disappear at the NAT.** The server never sees the request, the client never sees a response. From the application's view, the request hangs. 7. Eventually the request's context deadline fires: `context deadline exceeded`. The same shape applies to any stateful network appliance between you and the server: load balancers, corporate firewalls, IPSec tunnels. ## Solutions ### Rely on the defaults (preferred) If you can: use `http.DefaultTransport`, do not replace `rt.Transport`. `IdleConnTimeout=90s` and `Dialer.KeepAlive=30s` together cover the common NAT and firewall idle timeouts. No further configuration needed. ### Custom Transport — reinstate the defaults When you build a custom transport (for `TLSClientConfig`, an HTTP proxy URL, a `MaxIdleConnsPerHost` change, etc.), **start from the `http.DefaultTransport` values, then override only what you need**: ```go import ( "net" "net/http" "time" ) rt.Transport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, TLSClientConfig: yourTLSConfig, // <- your actual override } ``` The single most common bug is omitting the `Dialer`. A literal of the form `&http.Transport{TLSClientConfig: ...}` with no `DialContext` uses Go's net default dialler, which has **no keepalive at all**. ### Explicit `KeepAliveConfig` (Go 1.23+) The bare `net.Dialer.KeepAlive` field sets the probe interval. On Linux, the kernel does not start sending probes until a separate idle delay elapses, and that idle delay defaults to **7200 seconds** at the `tcp_keepalive_time` sysctl. With AWS NAT's 350s timeout, the probes never start in time. Go 1.23 introduced `net.Dialer.KeepAliveConfig`, which lets you set the idle delay explicitly so the kernel does not depend on `tcp_keepalive_time`: ```go DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAliveConfig: net.KeepAliveConfig{ Enable: true, Idle: 60 * time.Second, // wait 60s of idleness, then start probing Interval: 30 * time.Second, // send a probe every 30s Count: 4, // drop the conn after 4 missed probes }, }).DialContext, ``` With these numbers, after 60 seconds of silence the kernel starts sending probes, well before the 350s NAT timeout — the conntrack stays fresh, the application sees no surprises. ### Other levers - `http.Transport.IdleConnTimeout` set to less than the NAT timeout forces Go to close idle connections before NAT can drop them. The next request then dials fresh. - `http.Transport.DisableKeepAlives = true` opts out of HTTP keep-alive entirely — every request gets a fresh TCP connection. Simple and correct, but trades a handshake cost on every request. Reasonable for low-volume clients; pathological for high-volume ones. ## Diagnosing keep-alive problems When you suspect a keep-alive issue: - **Confirm the symptom shape.** "Context deadline exceeded" after a quiet period is the fingerprint of a dropped conntrack. If the failures happen *under load*, it's almost certainly something else. - **Check the Transport.** Print or log `rt.Transport` early in your application; if it is `*http.Transport`, inspect `IdleConnTimeout` and the dialler's `KeepAlive` / `KeepAliveConfig`. Many subtle bugs vanish at this step. - **Use `httptrace`.** The stdlib's [`net/http/httptrace`](https://pkg.go.dev/net/http/httptrace) package surfaces the connection lifecycle — `GotConn`, `PutIdleConn`, `ConnectStart`, `TLSHandshakeStart`, etc. When you see `GotConn` with `Reused: true` immediately followed by a hang, you have caught a stale pooled connection. (Future runtime versions may surface this via a built-in helper; see the roadmap.) - **On Linux, inspect kernel state.** `ss -t -o` shows the keepalive timer for each active socket; `cat /proc/sys/net/ipv4/tcp_keepalive_*` shows the kernel defaults; `conntrack -L` (where available) shows the NAT side. - **`tcpdump` on the client.** Look for outbound packets with no inbound response after the symptom appears. Confirms the NAT-drop hypothesis. ## Server-side, briefly A server's keep-alive behaviour is governed by [`http.Server`](https://pkg.go.dev/net/http#Server), not by anything in the runtime middleware: - `Server.IdleTimeout` — how long a kept-alive connection waits for the next request before the server closes it. - `Server.ReadHeaderTimeout`, `ReadTimeout`, `WriteTimeout` — bound the time spent on individual phases; expiry closes the connection. The runtime's server middleware does not override these. If your server sits behind a NAT or load balancer with an idle timeout, set `Server.IdleTimeout` to a value below that timeout so the server proactively closes idle connections — clients on Go will simply dial again on their next request without surfacing an error. ## Recipe — `Runtime` for cloud / NAT environments The construction below is the conservative starting point for a client deployed in AWS, GCP, or behind any stateful network appliance with an idle timeout. Adjust the timing constants if you have measurements; do not adjust them on intuition alone. ```go package main import ( "net" "net/http" "time" "github.com/go-openapi/runtime/client" ) func newClient(host, basePath string) *client.Runtime { rt := client.New(host, basePath, []string{"https"}) rt.Transport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, // Go 1.23+: explicit idle delay; bare KeepAlive=30s is // not enough on Linux because the kernel idle default // (tcp_keepalive_time) is often 7200s. KeepAliveConfig: net.KeepAliveConfig{ Enable: true, Idle: 60 * time.Second, Interval: 30 * time.Second, Count: 4, }, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 60 * time.Second, // < AWS NAT's 350s TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } return rt } ``` If you cannot move to Go 1.23, fall back to: ```go DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, IdleConnTimeout: 60 * time.Second, ``` and rely on the `IdleConnTimeout` to evict pooled connections before NAT does. The kernel keepalive probes may or may not fire in time depending on `tcp_keepalive_time`, but at least your idle pool is self-policing. ## Reference - [`net/http.Transport`](https://pkg.go.dev/net/http#Transport) - [`net.Dialer`](https://pkg.go.dev/net#Dialer), [`net.KeepAliveConfig`](https://pkg.go.dev/net#KeepAliveConfig) (Go 1.23+) - [`net/http/httptrace.ClientTrace`](https://pkg.go.dev/net/http/httptrace#ClientTrace) - Client transport: `client/runtime.go` (`Runtime.Transport`, `Runtime.New`) - The misnomer: `client/keepalive.go` ([`KeepAliveTransport`](https://pkg.go.dev/github.com/go-openapi/runtime/client#KeepAliveTransport), [`Runtime.EnableConnectionReuse`](https://pkg.go.dev/github.com/go-openapi/runtime/client#Runtime.EnableConnectionReuse)) - Issue #336: go-openapi-runtime-decad8f/docs/doc-site/tutorials/media-types.md000066400000000000000000000722151520232310000253140ustar00rootroot00000000000000--- title: Media-type selection weight: 20 description: | The reference for how the runtime parses, matches, and negotiates HTTP media types on both the server and client sides. Explains the rules behind a 415, a 406, or a 400 you see in production. --- How `go-openapi/runtime` parses, matches, and negotiates HTTP media types, on both the server and client sides. The reference for the rules behind a 415, a 406, or a 400 you see in production. > Scope: `Content-Type` and `Accept` headers, both inbound and outbound. > `Accept-Encoding` is mentioned briefly. Charset, language, and version > tags are treated as opaque parameters under the rules below. ## At a glance — error mapping | Outcome | HTTP | Where it's raised | |---|---|---| | Inbound `Content-Type` does not parse | **400** Bad Request | [`runtime.ContentType`](https://pkg.go.dev/github.com/go-openapi/runtime#ContentType), [`errors.ParseError`](https://pkg.go.dev/github.com/go-openapi/errors#ParseError) | | Inbound `Content-Type` is well-formed but not in the operation's `consumes` | **415** Unsupported Media Type | [`errors.InvalidContentType`](https://pkg.go.dev/github.com/go-openapi/errors#InvalidContentType) | | `Accept` cannot be satisfied by the operation's `produces` | **406** Not Acceptable | [`errors.InvalidResponseFormat`](https://pkg.go.dev/github.com/go-openapi/errors#InvalidResponseFormat) | | No consumer registered for an otherwise-allowed `Content-Type` | **500** Internal Server Error | server-side configuration error | ## The shared model — `mediatype.MediaType` Both sides use the same parser and value type: ```go import "github.com/go-openapi/runtime/server-middleware/mediatype" mt, err := mediatype.Parse("application/json;charset=utf-8;q=0.8") // mt.Type = "application" // mt.Subtype = "json" // mt.Params = {"charset": "utf-8"} // parameter keys lowercased // mt.Q = 0.8 // q is extracted, not stored in Params ``` ### Casing - `Type`, `Subtype`, parameter keys → lowercased on parse. - Parameter values → preserved verbatim. - Comparisons of parameter values are **case-insensitive** (`charset=UTF-8` matches `charset=utf-8`, the convention for charset, version, etc.). ### Wildcards `*/*` and `type/*` are accepted on either side of a comparison. `*/subtype` is invalid per RFC 7231 §5.3.2 and `Parse` rejects it. ### Malformed input Every `Parse` failure wraps the sentinel `mediatype.ErrMalformed`, so callers can distinguish "client sent garbage" from "client sent something well-formed that nothing here accepts": ```go _, err := mediatype.Parse(headerValue) if errors.Is(err, mediatype.ErrMalformed) { // 400 Bad Request territory } ``` ## The matching rule `MediaType.Matches(other)` is **asymmetric**. The receiver is the *bound* (an allowed entry on the server side, or a candidate offer when matching against an `Accept` entry); the argument is the *constraint* (the actual incoming request, or the `Accept` entry being satisfied). The rule: 1. Bare `type/subtype` must agree (with wildcards on either side). 2. If the receiver carries **no parameters**, any constraint is accepted regardless of its parameters. 3. Otherwise every `(key, value)` pair on the constraint must be present on the receiver, with case-insensitive value comparison. The receiver may carry **additional** parameters that the constraint does not list. q-values are **not** considered by `Matches` — they are the negotiator's concern, handled inside `Set.BestMatch`. The same direction is used in both call sites: | Call | Bound (receiver) | Constraint (argument) | |---|---|---| | Inbound validation | each entry in `consumes` | the request's `Content-Type` | | `Accept` negotiation | each candidate offer | each `Accept` entry | The asymmetry is intrinsic to the semantics ("loose if the bound has no params, otherwise the constraint must be a subset"), not to which side is the server. ## Beyond strict matching — alias and suffix tolerances The bare `Matches` rule above is strict RFC 7231: type, subtype, and the parameter subset. Two extensions sit on top of it, both surfaced through the graded result of `MediaType.Match`: | Tier | Reached when | Example | |---|---|---| | `MatchExact` | Strict RFC 7231 match. | `application/json` vs `application/json` | | `MatchAlias` | Strict fails but both sides resolve to the same canonical form via the package-internal alias table. | `application/x-yaml` vs `application/yaml` | | `MatchSuffix` | Strict and alias both fail but both sides resolve to the same base after folding the RFC 6839 structured-syntax suffix. | `application/vnd.api+json` vs `application/json` | | `MatchNone` | None of the above. | | `Set.BestMatch`, `MatchFirst`, and `mediatype.Lookup` rank candidates by this tier in addition to q-value and specificity — when two offers fit a constraint at different tiers, the stronger tier wins regardless of offer order. Exact beats alias, alias beats suffix. ### Alias bridge — always on RFC 9512 §2.1 enumerates three deprecated alias names for the `application/yaml` registration: | Alias | Canonical | |---|---| | `application/x-yaml` | `application/yaml` | | `text/yaml` | `application/yaml` | | `text/x-yaml` | `application/yaml` | A request, offer, or codec registration in any of these forms matches a counterpart in any of the others. The bridge is wire-format equivalence backed by an explicit IANA registration-template field — no opt-in needed and no way to disable it. ### Structured-syntax suffix tolerance — opt-in `+json`, `+xml`, and `+yaml` are the RFC 6839 structured-syntax suffixes the runtime recognises. Their wire format is the underlying base (`+json` is JSON), but their semantics carry application-specific structure on top (`application/problem+json` is JSON-on-the-wire with the RFC 7807 problem-details document shape). Tolerating these as equivalent to the base format is a contract loosening, so the runtime defaults to strict and surfaces the leniency through an explicit opt-in. Three matching knobs at three layers: ```go // per-call (in negotiation only) chosen := negotiate.ContentType(r, offers, "", negotiate.WithMatchSuffix(true), ) // server-wide ctx := middleware.NewContext(spec, api, nil).SetMatchSuffix(true) // client-wide rt := client.New(host, basePath, schemes) rt.MatchSuffix = true ``` All three feed the same `mediatype.AllowSuffix()` option through `Set.BestMatch`, `MatchFirst`, and `mediatype.Lookup`. With the flag on, a spec declaring `consumes: [application/json]` end-to-end tolerates request bodies sent with `Content-Type: application/vnd.api+json` (and likewise for `+xml` / `+yaml`). With the flag off — the default — such a request is rejected with 415, exactly as before. The opt-in is intended for situations where the user does not control both sides of the wire: - a server that wants to accept `application/problem+json` errors from upstream services declared as `application/json`; - a client that needs to consume `application/problem+json` responses from servers whose spec only declares `application/json` in `produces`. If both sides are under your control, **prefer to align the spec**: list `application/vnd.api+json` (or whichever variant applies) explicitly in `consumes` / `produces`. The opt-in is leeway for the common real-world mismatch, not a substitute for a faithful spec. ### Tier interactions worth pinning - **Parameters still bind at every tier.** A constraint of `application/yaml; charset=utf-8` does not match an offer of `application/yaml; charset=ascii` even with subtypes equal — the parameter-subset rule from `Matches` applies regardless of which tier resolved the subtype. Suffix tolerance does not loosen the param rule. - **Exact registrations always win.** If `application/vnd.api+json` is explicitly in `consumes` (or registered as a producer), routing and codec lookup never fall through to the suffix tier for that mime — even with `WithMatchSuffix(true)`. - **Map-side suffix folding is intentionally absent.** A registration at `application/vnd.api+json` does *not* receive a query of `application/json` even with the opt-in. The inverse case ("only the vendor consumer is registered, plain-base query arrives") is not a scenario the runtime tries to cover. ## Server side — inbound `Content-Type` validation Flow when a request arrives with a body: ``` runtime.HasBody(r) ── early-out for bodyless requests ↓ runtime.ContentType(r.Header) ── 400 here if the header is malformed ↓ validateContentType(consumes, ct) ├─ malformed actual → 400 errors.ParseError (defensive) ├─ no entry matches → 415 errors.InvalidContentType └─ match → continue to consumer dispatch ↓ route.Consumers[ct] ── 500 if no codec registered ``` `validateContentType` is a thin wrapper around [`mediatype.MatchFirst`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/mediatype#MatchFirst). It short-circuits on the first allowed entry that accepts the actual — not the most specific match. For ranked matching use `Set.BestMatch`. ### What "missing `Content-Type`" does When the request body is non-empty but the header is missing, `runtime.ContentType` substitutes the package-level default (`runtime.DefaultMime` = `application/octet-stream`). The validator then matches that default against the operation's `consumes`. So a request with a body and no `Content-Type` typically yields **415** unless the operation lists `application/octet-stream`. ### Parameter honouring (since v0.30) Before v0.30, parameters were stripped on both sides before matching: `Content-Type: text/plain;charset=ascii` would pass against `consumes: [text/plain;charset=utf-8]`. Since v0.30 this is rejected (charset values disagree). The fix landed with PR #426 (issue #136). ## Server side — outbound `Accept` negotiation [`negotiate.ContentType(r, offers, defaultOffer, opts...)`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/negotiate#ContentType) reads the request's `Accept` header(s), parses each entry, ranks the offers, and returns the winning offer (a string from the `offers` slice). If nothing matches, `defaultOffer` is returned. ### Ranking Per RFC 7231 §5.3.2, in order: 1. Highest **q-value** (`q=0` excludes an offer entirely). 2. Highest **specificity** of the matched `Accept` entry (`type/subtype;params` > `type/subtype` > `type/*` > `*/*`). 3. Earliest position in the `offers` slice. ### Multiple `Accept` headers Per RFC 7230 §3.2.2, multiple `Accept` headers are equivalent to a single comma-joined value. The negotiator joins before parsing, so all entries contribute to the decision regardless of how the client batched them. ### Parameter honouring and the opt-out Same v0.30 change as inbound validation. An `Accept` entry of `text/plain;charset=utf-8` matches an offer of bare `text/plain` (offer carries no constraint), but **not** `text/plain;charset=ascii`. To restore the looser pre-v0.30 behaviour for one operation: ```go chosen := negotiate.ContentType(r, offers, "", negotiate.WithIgnoreParameters(true), ) ``` …or server-wide, threaded through the middleware `Context`: ```go ctx := middleware.NewContext(spec, api, nil).SetIgnoreParameters(true) ``` The opt-out exists for applications whose producers and `Accept` clients use mismatched charset or version params that they treat as informational. ### Codec dispatch is keyed by bare type The negotiator returns the verbatim offer (parameters preserved) and the runtime sets `Content-Type` from it. Codec dispatch is a separate step: the runtime looks up the producer in `route.Producers`, which is a `map[string]Producer` keyed by the **bare** `type/subtype` (no params). You will see calls to `normalizeOffer(format)` and `normalizeOffers(...)` in the middleware and the router doing exactly this stripping — they are about map lookup, not about negotiation. The practical consequence: you cannot register two different producers for the same bare type that differ only by parameters (`text/plain;charset=utf-8` vs `text/plain;charset=ascii`). They would collide on the bare-type key. The negotiator can still **choose** between two such offers (parameters are honoured during matching), but the codec invoked is the single one registered under the bare key. If you need parameter-specific encoding, do it inside one producer and inspect the negotiated `Content-Type` from the response writer. ## Client side — outbound `Content-Type` Selection runs in two stages. Stage 1 picks a candidate from the operation's `consumes` list before the payload is known; Stage 2 runs inside `buildHTTP` after the request writer has populated the payload, and may upgrade Stage 1's choice when the payload is a stream. ### Stage 1 — `pickConsumesMediaType` Source: `client/runtime.go`. ```go cmt := pickConsumesMediaType(operation.ConsumesMediaTypes, r.Producers, r.DefaultMediaType) ``` 1. If `multipart/form-data` is one of the entries, prefer it (it streams and preserves per-file `Content-Type`). Resolves issue #286. 2. Otherwise the first non-empty entry that is **either** a structural mime (`multipart/form-data`, `application/x-www-form-urlencoded`) **or** has a producer registered in `Runtime.Producers`. This skips spec entries the client cannot serialise — useful when the spec lists a vendor mime first and a registered alternative second. Closes part of issues #32 and #386. 3. If nothing in the list is registered, the first non-empty entry is returned anyway so the gate at the call site emits its `none of producers: …` diagnostic. 4. Falls back to `Runtime.DefaultMediaType` (`application/json` by default) only when the list is empty (or all empty strings). Stage 1 cannot see the payload — the request writer hasn't run yet — so its choice is "best effort given only the spec and the registered producers." ### Stage 2 — `setStreamContentType` Source: `client/request.go`. Runs inside `buildHTTP` after the writer has populated `r.payload`. For stream payloads (`io.Reader`, `io.ReadCloser`) only — the producer is bypassed in this branch, so the wire header is the only place where the body's actual MIME type is asserted. Three checks, in priority order: 1. **Explicit `SetHeaderParam("Content-Type", …)`.** The historical header escape hatch wins over every derivation. If the writer set `Content-Type` during `WriteToRequest`, the runtime keeps it as-is. This was not the original purpose of `SetHeaderParam`, but it has become the natural way to say "send THIS exact header", and we honour it. Caveat: the user is then responsible for matching their declared header to their actual body bytes. 2. **Payload-declared content type.** If `r.payload` implements the exported [`runtime.ContentTyper`](https://pkg.go.dev/github.com/go-openapi/runtime#ContentTyper) interface and returns a non-empty value, that value wins. The value declares its own nature — useful for line-delimited formats, custom MIME types, or any case where the spec offers no matching entry. The same interface is also consulted on each part of a multipart file upload. 3. **Octet-stream upgrade.** When neither of the above applies, and `application/octet-stream` is in the operation's `consumes` list AND a producer is registered for it, the wire header is upgraded from the picker's choice to octet-stream — a safer "raw bytes" claim than a structural mime like JSON. If none of the three checks fire, the picker's `mediaType` from Stage 1 is used as the terminal fallback. #### Non-stream paths are deliberately not honoured `SetHeaderParam("Content-Type", …)` and `runtime.ContentTyper` are honoured **only** for stream payloads. Non-stream paths have structural constraints that conflict with arbitrary user-supplied content types: - **`struct` / `[]byte` payloads** — the producer is dispatched off `mediaType`. Honouring an arbitrary user header here would mean either swapping the producer (complex) or sending a body that doesn't match the declared header (still a lie). - **Multipart bodies** — the runtime owns the `Content-Type` header because of the boundary parameter requirement. - **URL-encoded forms** — the body is form-encoded; lying about the type would break parsing on the server. Users with these payload shapes who need a custom content type should adjust the operation's `consumes` list (so the picker selects the right entry) or register a producer under the desired MIME. ### Wire `Content-Type` matrix | Payload | `SetHeader Content-Type` | declares `ContentType()` | octet-stream offered + registered | Wire `Content-Type` | |---|---|---|---|---| | stream | set | — | — | the SetHeader value | | stream | unset | yes, non-empty | — | declared value | | stream | unset | no / empty | yes | `application/octet-stream` | | stream | unset | no / empty | no | picker's choice (best-effort; may misrepresent body) | | `struct` | (ignored) | — | — | picker's choice (producer runs) | | `[]byte` | (ignored) | — | — | picker's choice (producer runs; e.g. JSON producer base64-encodes) | ### Declaring a stream's MIME type Wrap the reader in a type that satisfies [`runtime.ContentTyper`](https://pkg.go.dev/github.com/go-openapi/runtime#ContentTyper): ```go type ndjsonStream struct { io.Reader } func (n *ndjsonStream) ContentType() string { return "application/x-ndjson" } // in your params writer: return r.SetBodyParam(&ndjsonStream{Reader: myReader}) ``` The wire `Content-Type` will be `application/x-ndjson` regardless of which entry the picker chose from the operation's `consumes`. ### Codec registration The client transport ships with a fixed codec set (JSON, YAML, XML, CSV, text, HTML, byte-stream). Register additional MIME types directly: ```go rt := client.New(host, basePath, schemes) rt.Consumers["application/problem+json"] = runtime.JSONConsumer() rt.Producers["application/problem+json"] = runtime.JSONProducer() ``` See [FAQ § custom MIME types](../faq/#how-do-i-register-custom-mime-types-eg-applicationproblemjson). ### Known gaps - **Issue [#385](https://github.com/go-openapi/runtime/issues/385) / [#33](https://github.com/go-openapi/runtime/issues/33)** — The codec set is hardcoded; it is not derived from the spec. Apps that don't declare an exotic `consumes`/`produces` carry codecs they will never use. Tracked as Track A.2 in the modularization roadmap. - **`[]byte` payloads.** A `[]byte` flows through the picker's chosen producer. The JSON producer base64-encodes it as a JSON string. If you want raw bytes on the wire, wrap as `bytes.NewReader([]byte{…})` — it then takes the stream path and the Stage-2 octet-stream upgrade applies. ### What changed in v0.30 (client-side outbound) Four behaviour deltas vs. v0.29. Three are confined to **stream payloads** (`io.Reader`, `io.ReadCloser`); the fourth touches the Stage-1 picker for any payload type. The first three surface only when there is at least one stream payload involved; existing client code that uses generated parameter types with `struct`/`[]byte` payloads is unaffected by those. | Delta | Pre-v0.30 (master) | v0.30 | |---|---|---| | Body payload's `ContentType()` | not consulted; picker's `mediaType` is sent | when the payload satisfies [`runtime.ContentTyper`](https://pkg.go.dev/github.com/go-openapi/runtime#ContentTyper), its non-empty return value becomes the wire `Content-Type` | | Stage-2 octet-stream upgrade | absent; the picker's choice is the only signal | when the payload is a stream and lacks an explicit declaration, `application/octet-stream` from the operation's `consumes` list is used in preference to a structural mime like `application/json` | | `SetHeaderParam("Content-Type", X)` | silently overwritten by `buildHTTP` | honoured at top priority; the user's explicit assertion wins | | Stage-1 producer-capability filter | picker returns the first non-empty entry; if no producer is registered for it, the gate at the call site errors | picker skips entries with no registered producer (and no structural status) and tries the next one; only errors when nothing in `consumes` is registered | Each delta is verified by a row in the behavioural harness at [`client/content_negotiation_test.go`](https://github.com/go-openapi/runtime/blob/master/client/content_negotiation_test.go). The rows that fail when the harness runs against the v0.29 baseline are exactly the rows that exercise these three deltas — there are no incidental behaviour changes outside this set. The structural paths (form, multipart, file uploads) and the multipart-vs-urlencoded preference fix from #286 are preserved verbatim. #### Migration notes - **No action needed** for callers using `struct`-typed parameters generated by go-swagger. The wire `Content-Type` is unchanged. - **Streams that need a specific MIME type** can implement [`runtime.ContentTyper`](https://pkg.go.dev/github.com/go-openapi/runtime#ContentTyper) on the payload value, or add `application/octet-stream` to the operation's `consumes`, or fall back to setting the header explicitly via the params writer. - **Callers that relied on `SetHeaderParam("Content-Type", …)` and found it didn't work** (it never did, on body requests) can now rely on it as a documented escape hatch for stream payloads. ## Client side — inbound responses There is no `Accept` negotiation step at decode time. The client sent its `Accept` header on the request and is now reading whatever the server chose to return — the response's `Content-Type` header is the single input the codec dispatcher consults. ### Pipeline ``` response.Header["Content-Type"] │ ▼ resolveConsumer(ct) ── client/runtime.go │ ▼ picks a runtime.Consumer operation.Reader ── codegen-emitted; switches on status code, │ hands the body to the picked consumer, ▼ decodes into the typed response struct typed response value or error ``` The codegen-emitted **operation `Reader`** is the piece most users never see. It's a generated function per operation that: 1. Reads the HTTP status code and selects the matching response definition from the spec. 2. Calls `runtime.ContentType(response.Header)` to extract the bare mime. 3. Invokes the runtime to resolve a consumer for that mime (`resolveConsumer`). 4. Decodes the body into the response definition's Go type via `consumer.Consume(body, target)`. If you are writing a custom client without codegen, you implement this function yourself. ### `resolveConsumer` — picking a consumer `resolveConsumer(ct string)` in `client/runtime.go` is the single codec-lookup site on the client. It runs: 1. Parse `ct` (rejects malformed values with a `"parse content type: …"` error — surfaced as a client-side error, not as a server response). 2. `mediatype.Lookup(r.Consumers, ct, r.matchOpts()...)` — runs the four always-on tiers (raw key, parsed canonical, alias query-side, alias map-side) plus the opt-in suffix tier when `Runtime.MatchSuffix` is set. See "Beyond strict matching" above. 3. On lookup miss, fall back to `r.Consumers["*/*"]` if a wildcard consumer is registered. 4. On full miss, return `"no consumer: %q"` — the operation `Reader` propagates this as the operation's error. ### Where `Runtime.MatchSuffix` lands Setting `rt.MatchSuffix = true` flips the inbound decode path to tolerate RFC 6839 suffix media types: a response with `Content-Type: application/problem+json` finds the JSON consumer registered at `application/json`, decoded into whatever Go type the response definition declares. The wildcard `"*/*"` fallback runs unchanged after the suffix tier. Symmetric to the server-side `Context.SetMatchSuffix(true)` — the opt-in is independent on each side and exists for exactly the same reason: real servers (or real clients) that don't strictly abide by the spec's `produces` / `consumes` declarations. ### Alias bridge — also active here The always-on alias bridge applies on this path too. A client that registers the YAML consumer at the legacy `application/x-yaml` key (or, for that matter, leaves the default-map flip in place at `application/yaml`) handles a server response with `Content-Type: text/yaml` correctly — `mediatype.Lookup` canonicalizes both keys to `application/yaml` and finds the consumer regardless of which form was registered. ### Failure modes worth knowing - **Malformed `Content-Type`** (e.g. trailing garbage, unterminated quoted string) — `resolveConsumer` returns an error sourced from `mime.ParseMediaType`, prefixed with `parse content type:`. The operation `Reader` surfaces this as the operation's error; no decode is attempted. - **No consumer, no wildcard registered** — `"no consumer: %q"` with the offending Content-Type. Most commonly hit when the server returns an undeclared error mime (`application/problem+json` is the canonical example) and `Runtime.MatchSuffix` is off and `"*/*"` is not registered. - **Silent wildcard fallback** — if `Consumers["*/*"]` is registered (the default-map registers `runtime.ByteStreamConsumer` there), any unrecognised `Content-Type` decodes through that consumer. For a typed response struct, this usually fails inside the consumer's own unmarshal with a less specific error than the no-consumer case. Worth knowing if the runtime appears to "silently succeed at decoding garbage." ## `Accept-Encoding` The runtime does not handle `Accept-Encoding` or transparently encode response bodies. The historical `negotiate.ContentEncoding` helper is deprecated — it was never paired with a real encoder, so on its own it produces no `Vary`, no `Content-Length` rewrite, and no minimum-size guard. Compose a proper compression middleware at the `http.Handler` level instead: the [compression recipe](../../usage/examples/middleware/compression/) wraps `middleware.Serve` with [`CAFxX/httpcompression`](https://github.com/CAFxX/httpcompression). ## Common gotchas **"My matching test broke after upgrading to v0.30."** Likely the parameter-honouring change. If your `Accept` clients and your `produces` use mismatched charset/version params and you treat those as informational, opt out with `negotiate.WithIgnoreParameters(true)` (per call) or `Context.SetIgnoreParameters(true)` (server-wide). **"My server rejects `application/vnd.api+json` (or `application/problem+json`) with 415."** The default match is strict RFC 7231 — a vendor `+json` mime is *not* a `application/json` mime. Two routes forward: (1) list the vendor mime explicitly in the operation's `consumes` and register a codec under that key (the spec-faithful path); or (2) enable `Context.SetMatchSuffix(true)` server-wide to fold `+json` / `+xml` / `+yaml` to the underlying base codec at lookup time (the leeway path, for situations where the client is not under your control). See the "Beyond strict matching" section above. **"My client request returns 415 even though the API lists my type in `consumes`."** Check the wire `Content-Type` against your server's `consumes` matching rules. The client sends the picker's choice (with Stage-2 upgrades for streams), so a stray space, missing charset, or trailing `;` in the spec entry will be sent through and rejected by a strict server. If the payload is a stream, consider implementing `ContentType() string` on it to declare the type explicitly. **"My stream payload's wire `Content-Type` is wrong."** Four cases in priority order: set the header explicitly via `SetHeaderParam("Content-Type", …)` in your params writer; implement `runtime.ContentTyper` (`ContentType() string`) on the payload to declare an explicit type; add `application/octet-stream` to the operation's `consumes` list to trigger the Stage-2 upgrade; or list the desired mime first in `consumes` so the picker chooses it. **"My server returns 400 for a missing `Content-Type` on a body request."** It shouldn't — missing headers fall through to `application/octet-stream` via `runtime.DefaultMime` and that produces 415, not 400. A 400 means the header is *present and unparseable*. Check for stray characters (unmatched parens, wildcards in parameter names, etc.). **"How do I get the parsed `Content-Type` value in my handler?"** Use [`runtime.ContentType(r.Header)`](https://pkg.go.dev/github.com/go-openapi/runtime#ContentType) or the cached value at `middleware.MatchedRouteFrom(r).Consumes`. ## Reference - Server matching primitive: `github.com/go-openapi/runtime/server-middleware/mediatype` - Server negotiator: `github.com/go-openapi/runtime/server-middleware/negotiate` - Codec lookup helper: `mediatype.Lookup[T]` — used by both server (`middleware/context.go`, `middleware/validation.go`) and client (`client/runtime.go`) - Alias and suffix tolerances: `mediatype.Match`, `mediatype.MatchKind`, `mediatype.AllowSuffix`; opt-in surfaces `negotiate.WithMatchSuffix`, `middleware.Context.SetMatchSuffix`, `client.Runtime.MatchSuffix` - Server validation: `middleware/validation.go` (`validateContentType`) - Client Stage-1 picker: `client/runtime.go` (`pickConsumesMediaType`) - Client Stage-2 fallback: `client/request.go` (`setStreamContentType`, `streamFallbackMime`, `payloadContentType`) - Behavioural test harness: `client/content_negotiation_test.go` - RFC 7231 §3.1.1 (media type), §5.3.1 (q-values), §5.3.2 (Accept). go-openapi-runtime-decad8f/docs/doc-site/usage/000077500000000000000000000000001520232310000216205ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/usage/_index.md000066400000000000000000000006511520232310000234120ustar00rootroot00000000000000--- title: Usage description: | Guides, references and examples for using github.com/go-openapi/runtime in clients, servers and standalone middleware. weight: 2 --- Pick a section below. The [core](./core/) pages explain the interfaces and layering that the rest of the library is built on — start there if you want a mental model before diving into any specific area. {{< children type="card" description="true" >}} go-openapi-runtime-decad8f/docs/doc-site/usage/client/000077500000000000000000000000001520232310000230765ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/usage/client/_index.md000066400000000000000000000025611520232310000246720ustar00rootroot00000000000000--- title: Client weight: 20 description: | HTTP client transport — TLS, auth, OpenTelemetry tracing and request submission for go-swagger-generated and untyped API clients. --- The `client` package provides [`client.Runtime`](https://pkg.go.dev/github.com/go-openapi/runtime/client#Runtime), the configurable HTTP transport that go-swagger-generated clients use under the hood. You can also drive it directly for untyped API calls. A minimal client looks like this: {{< code file="client/intro/main.go" lang="go" region="minimalClient" >}} What the four pages below cover: | Page | About | |---------------------------------------------|-----------------------------------------------------------------------------| | [Transport](./transport/) | `Runtime` configuration: TLS, timeouts, proxy, keepalive, debug logging | | [Authentication](./auth/) | `ClientAuthInfoWriter` and the built-in helpers (Basic, API key, Bearer) | | [Tracing](./tracing/) | OpenTelemetry support and the legacy OpenTracing compat module | | [Building & submitting requests](./requests/) | `ClientOperation`, `Submit` vs `SubmitContext`, the v0.30 context-only pivot | {{< children type="card" description="true" >}} go-openapi-runtime-decad8f/docs/doc-site/usage/client/auth.md000066400000000000000000000072011520232310000243610ustar00rootroot00000000000000--- title: Authentication weight: 20 description: | Attaching auth information to outgoing requests — Basic, API key, Bearer and OAuth2. --- Client-side authentication is a pure encoding concern: take some credentials, write the right header / query parameter on the outbound request. It is decoupled from the server-side `Authenticator` / `Authorizer` interfaces ([core / interfaces](../../core/interfaces/)) — those answer "is this request allowed?", these answer "how do I sign it?". ## The interface — `ClientAuthInfoWriter` ```go package runtime type ClientAuthInfoWriter interface { AuthenticateRequest(ClientRequest, strfmt.Registry) error } type ClientAuthInfoWriterFunc func(ClientRequest, strfmt.Registry) error ``` See [`runtime.ClientAuthInfoWriter`](https://pkg.go.dev/github.com/go-openapi/runtime#ClientAuthInfoWriter) for the authoritative definition. Anything with that signature can be used as auth. The `ClientRequest` argument exposes `SetHeaderParam`, `SetQueryParam`, `SetBodyParam` — i.e. the same surface generated parameter types use to encode themselves. ## Where to attach it Two places, with predictable precedence: {{< code file="client/auth/main.go" lang="go" region="attachAuth" >}} The runtime calls the operation's `AuthInfo` if set, otherwise the runtime's `DefaultAuthentication`. Either may be nil for unauthenticated endpoints. ## Built-in helpers All four return a ready-to-use `ClientAuthInfoWriter`. ### `BasicAuth(user, password)` — RFC 7617 {{< code file="client/auth/main.go" lang="go" region="basicAuth" >}} Sets `Authorization: Basic `. ### `APIKeyAuth(name, in, value)` — RFC-undefined but ubiquitous {{< code file="client/auth/main.go" lang="go" region="apiKeyAuth" >}} `in` must be `"header"` or `"query"`. Anything else returns nil — at which point you'll silently send the request unauthenticated, so check your spelling. ### `BearerToken(token)` — RFC 6750 OAuth2 access tokens {{< code file="client/auth/main.go" lang="go" region="bearerAuth" >}} Sets `Authorization: Bearer `. For OAuth2 client flows that need to acquire and refresh the token, build the writer around an `oauth2.TokenSource` from `golang.org/x/oauth2` and re-attach it on every call (or use a custom writer that calls `Token()`). ### `Compose(auths…)` — combine multiple writers For APIs that require more than one credential header on the same request — say an API key plus a bearer token — chain them: {{< code file="client/auth/main.go" lang="go" region="composeAuth" >}} Nil writers in the list are skipped silently. The first non-nil writer that returns an error short-circuits the chain. ### `PassThroughAuth` — explicit "no auth" A no-op writer. Use it when the operation requires *some* writer (for instance because it's defined as `security: [[]]` in the spec) but no actual credential should be attached. {{< code file="client/auth/main.go" lang="go" region="passThroughAuth" >}} ## Writing your own A common case: an HMAC-signed request that needs to compute the signature over the body. Implement `ClientAuthInfoWriter` directly: {{< code file="client/auth/main.go" lang="go" region="hmacSignature" >}} The runtime calls `AuthenticateRequest` after the operation's parameters have been bound but before the request is sent — so `r.GetBody()` returns the encoded body for buffered payloads. For streaming bodies (multipart, raw streams) the runtime arranges a body-copy closure so the signer sees the bytes that will go on the wire; see `BuildHTTPContext` in [`client/internal/request`](https://pkg.go.dev/github.com/go-openapi/runtime/client/internal/request) for the gory details. go-openapi-runtime-decad8f/docs/doc-site/usage/client/requests.md000066400000000000000000000132631520232310000253000ustar00rootroot00000000000000--- title: Building & submitting requests weight: 40 description: | ClientOperation, BuildHTTP and SubmitContext — and the recent pivot to context-only request building. --- `Runtime` exposes a small set of entry points for turning a `runtime.ClientOperation` into a sent request and a typed result. The public surface has been pivoting from "use the cached context on the operation/runtime" to "pass the context explicitly". This page covers both shapes and explains which to use when. ## The descriptor — `ClientOperation` Authoritative definitions live in the [`runtime`](https://pkg.go.dev/github.com/go-openapi/runtime#ClientOperation) package: ```go package runtime type ClientOperation struct { ID string Method string PathPattern string ProducesMediaTypes []string ConsumesMediaTypes []string Schemes []string AuthInfo ClientAuthInfoWriter Params ClientRequestWriter Reader ClientResponseReader Context context.Context // legacy — see below Client *http.Client // optional per-call override } type ClientTransport interface { Submit(*ClientOperation) (any, error) } ``` Generated clients build one of these per operation method and call `Submit` (or, increasingly, `SubmitContext`). For untyped use you populate the fields by hand. ## Entry points The runtime offers four methods, paired by purpose: | Purpose | Legacy (cached ctx) | Context-aware (preferred) | |------------------------------------------|------------------------------------|----------------------------------------------------| | Send the request, return the typed result | `Runtime.Submit(op)` | `Runtime.SubmitContext(ctx, op)` | | Build the `*http.Request` only | `Runtime.CreateHttpRequest(op)` ⚠ | `Runtime.CreateHTTPRequestContext(ctx, op)` | ⚠ `CreateHttpRequest` is **deprecated**. It does not return the context's cancel function, so any per-request timeout set via `Params.SetTimeout` is silently leaked. Use `CreateHTTPRequestContext` instead. ### `Submit` vs `SubmitContext` `Submit` consults its context in this order: 1. `op.Context` if non-nil 2. otherwise `rt.Context` 3. otherwise `context.Background()` `SubmitContext(ctx, op)` ignores those cached values entirely and uses `ctx` as the parent context. This is the only way to pass a caller-controlled context that can be cancelled, deadlined or trace-instrumented from the call site. {{< code file="client/requests/main.go" lang="go" region="submitVariants" >}} The per-request timeout set via `Params.SetTimeout(d)` (i.e. `runtime.ClientRequestWriter.SetTimeout`) is honoured by **both** forms — it is applied when the request context is derived inside `BuildHTTPContext`, on top of whatever deadline `ctx` already carries. ### Build-only — `CreateHTTPRequestContext` When you need the prepared `*http.Request` but want to drive `http.Client.Do` yourself (for retries, custom logging, response-body inspection), use: {{< code file="client/requests/main.go" lang="go" region="createHTTPRequestContext" >}} `cancel` releases the per-request timeout timer and any other resources held by the derived context. **Calling it before the response body is fully drained will cancel the in-flight request** — defer it to the end of the read. On error the returned cancel is a no-op, so deferring it unconditionally is safe. ## What happens during a `SubmitContext` call {{< mermaid align="center" zoom="true" >}} flowchart TD in(((SubmitContext ctx, op))) prep["prepareRequest
resolve scheme + media type
pick AuthInfoWriter (op.AuthInfo or rt.DefaultAuthentication)"] build["BuildHTTPContext
WriteToRequest → ctx with timeout
buffered or streaming body
AuthenticateRequest"] do["http.Client.Do"] decode["resolveConsumer · ReadResponse
decode into typed result"] out(((result, err))) cancel["cancel()
(deferred)"] in --> prep --> build --> do --> decode --> out build -.-> cancel {{< /mermaid >}} `BuildHTTPContext` chooses one of two assembly paths: - **buffered body** — for URL-encoded forms, producer output, or no body. The body is materialised in memory before `AuthenticateRequest` runs, so writers like HMAC signers see the final bytes. - **streaming body** — for multipart uploads or stream payloads (`io.Reader` body). The body flows through an `io.Pipe`. Auth writers receive a body-copy closure so signers can still see the bytes — at the cost of one extra read. ### Multipart uploads honour context cancellation A long-standing rough edge — the multipart upload goroutine ignoring the request context — was fixed in `feat(client): honor context cancellation in multipart upload goroutine`. Cancelling the context mid-upload now stops the writer goroutine cleanly instead of leaking it for the lifetime of the connection. ## Migration from the legacy form If your codebase calls `Submit` and stashes contexts on `op.Context` or `rt.Context`, the change is usually mechanical: {{< code file="client/requests/main.go" lang="go" region="migrationForm" >}} `op.Context` and `rt.Context` are still read by `Submit` for compatibility with existing callers and generated code that has not yet been regenerated; `SubmitContext` ignores both. New code (and freshly regenerated clients) should pass the context explicitly. For `CreateHttpRequest` callers the move is more important — the deprecated form leaks the per-request timer when `Params.SetTimeout` is non-zero. Switch to `CreateHTTPRequestContext` and remember to defer the returned `cancel`. go-openapi-runtime-decad8f/docs/doc-site/usage/client/tracing.md000066400000000000000000000064441520232310000250570ustar00rootroot00000000000000--- title: Tracing weight: 30 description: | Built-in OpenTelemetry support on client.Runtime, plus a note on the legacy OpenTracing compatibility module. --- `client.Runtime` ships first-class OpenTelemetry support. There are no extra modules to import beyond the runtime itself (it already depends on `go.opentelemetry.io/otel`). ## Wire it up — `WithOpenTelemetry` See [`client.Runtime.WithOpenTelemetry`](https://pkg.go.dev/github.com/go-openapi/runtime/client#Runtime.WithOpenTelemetry) for the authoritative signature: func (r *Runtime) WithOpenTelemetry(opts ...OpenTelemetryOpt) runtime.ClientTransport Returns a `runtime.ClientTransport` that delegates to the underlying runtime and creates a client span for every request. Use it as the transport you hand to a generated client: {{< code file="client/tracing/main.go" lang="go" region="wireOpenTelemetry" >}} For untyped use you call `traced.Submit(op)` directly. ### A span only appears when one is already active If the operation's context does not contain an active span, the transport does **not** start a root span. This is intentional — telemetry boundaries belong to the application, not to the transport library. Wrap your call site in a span and the client span attaches beneath it. ## Options — `OpenTelemetryOpt` | Option | What it sets | Default | |--------------------------------|----------------------------------------------------------------------------------------------------|--------------------------------------------------| | `WithTracerProvider(provider)` | The `trace.TracerProvider` to acquire a tracer from. | the global provider (`otel.GetTracerProvider`) | | `WithPropagators(ps)` | The `propagation.TextMapPropagator` used to inject context into outbound headers. | the global propagator (`otel.GetTextMapPropagator`) | | `WithSpanOptions(opts…)` | Extra `trace.SpanStartOption`s applied to every new span (kind, attributes, etc.). | none | | `WithSpanNameFormatter(fn)` | Function that derives the span name from the `*runtime.ClientOperation`. | `op.ID` if non-empty, otherwise `"{method}_{pathPattern}"` | Example with a custom name and global tags: {{< code file="client/tracing/main.go" lang="go" region="customSpanFormatter" >}} ## Legacy OpenTracing `Runtime.WithOpenTracing` exists but is **deprecated**. It silently returns an OpenTelemetry transport, ignoring opts that are not `OpenTelemetryOpt`. The OpenTracing project is archived — new code should call `WithOpenTelemetry`. If you still need OpenTracing semantics (for example because your collector is OpenTracing-only), import the compatibility add-on: ```sh go get github.com/go-openapi/runtime/client-middleware/opentracing ``` ```go import ( "github.com/go-openapi/runtime/client-middleware/opentracing" ottrace "github.com/opentracing/opentracing-go" ) traced := opentracing.WithOpenTracing(rt, ottrace.GlobalTracer()) ``` The compat module lives in its own Go module so the rest of the runtime no longer pulls the OpenTracing dependency. go-openapi-runtime-decad8f/docs/doc-site/usage/client/transport.md000066400000000000000000000135051520232310000254600ustar00rootroot00000000000000--- title: Transport weight: 10 description: | Configuring client.Runtime — TLS, timeouts, proxy, keepalive and the underlying http.Client. --- [`client.Runtime`](https://pkg.go.dev/github.com/go-openapi/runtime/client#Runtime) wraps a `*http.Client` plus the wire-format codecs needed to call an OpenAPI-described API. This page covers the knobs that shape the underlying HTTP behaviour; auth, tracing and request submission live on their own pages. ## Constructors ```go func New(host, basePath string, schemes []string) *Runtime func NewWithClient(host, basePath string, schemes []string, client *http.Client) *Runtime ``` See [`client.New`](https://pkg.go.dev/github.com/go-openapi/runtime/client#New) and [`client.NewWithClient`](https://pkg.go.dev/github.com/go-openapi/runtime/client#NewWithClient) for the authoritative signatures. `New` builds a runtime against `http.DefaultTransport`. `NewWithClient` takes an explicit `*http.Client` — use it when you need a non-default transport (custom TLS, a proxy, an instrumented round-tripper, etc.) or want to share a client across runtimes. `schemes` lists allowed URL schemes (`"https"`, `"http"`); the runtime picks one when building a request, preferring HTTPS. ## What `New` sets up for you | Field | Default | |----------------------------|-------------------------------------------------------------------------------------------| | `DefaultMediaType` | `application/json` (`runtime.JSONMime`) | | `Consumers` / `Producers` | JSON, XML, YAML, plain text, HTML, CSV and `application/octet-stream` byte stream codecs. | | `Transport` | `http.DefaultTransport` | | `Context` | `context.Background()` (legacy field — see [requests](../requests/)) | | `Debug` | enabled if `SWAGGER_DEBUG` or `DEBUG` env var is set | You can replace any of these after construction. Example: register a custom codec for a vendor JSON content type that the client will encounter on responses. {{< code file="client/transport/main.go" lang="go" region="registerVendorCodec" >}} ## TLS — `TLSClientAuth` and `TLSClientOptions` For mutual TLS, custom CAs or certificate pinning, build a `*tls.Config` via [`TLSClientAuth`](https://pkg.go.dev/github.com/go-openapi/runtime/client#TLSClientAuth): {{< code file="client/transport/main.go" lang="go" region="setupMutualTLS" >}} Option highlights (the full struct is in the godoc): | Group | Fields | |----------------------|-----------------------------------------------------------------------------------------------------| | Client cert (paths) | `Certificate`, `Key` | | Client cert (loaded) | `LoadedCertificate`, `LoadedKey` | | Server CAs | `CA`, `LoadedCA`, `LoadedCAPool` (combined with each other; otherwise the system pool is used) | | Hostname / verify | `ServerName`, `InsecureSkipVerify` (ignored when `ServerName` is set), `VerifyPeerCertificate`, `VerifyConnection` | | Resumption | `SessionTicketsDisabled`, `ClientSessionCache` | `TLSClientAuth` always returns a config with `MinVersion = TLS 1.2`. ## Timeouts Two layers of timeout apply: 1. **Per-request timeout** — set via the operation's `Params.SetTimeout(d)` (any generated parameter type implements this). This becomes the deadline of the request `context.Context` derived inside `BuildHTTPContext` (see [requests](../requests/)). 2. **HTTP client timeout** — set on the `*http.Client` you pass to `NewWithClient`. This is the standard `Client.Timeout` field; it applies regardless of the per-request value. There is also a package-level `DefaultTimeout = 30 * time.Second`. It is **not** wired up automatically; it exists for callers building their own `*http.Client` that want to use the same default the runtime advertises. {{< code file="client/transport/main.go" lang="go" region="timeoutClient" >}} ## Proxy Proxy configuration lives on the underlying `*http.Transport`, not on `Runtime`. Two common patterns: 1. Honour `HTTPS_PROXY` / `HTTP_PROXY` (default behaviour anyway): {{< code file="client/transport/main.go" lang="go" region="proxyFromEnv" >}} 2. Force a specific proxy: {{< code file="client/transport/main.go" lang="go" region="proxyExplicit" >}} ## Keepalive — `EnableConnectionReuse` Some servers never close the response body, which prevents Go from reusing the underlying TCP connection. `EnableConnectionReuse` installs a transport middleware that drains the unread body on `Close()` so the connection can return to the pool: {{< code file="client/transport/main.go" lang="go" region="enableConnectionReuse" >}} This is **not** enabled by default because for some servers the response stream never completes and draining would block forever. Turn it on when you've confirmed the server you're talking to does the right thing. ## Debug logging Two ways to enable wire-level dumps of requests and responses (both go through `httputil.DumpRequest` / `DumpResponse`): - Set the `SWAGGER_DEBUG` (or `DEBUG`) environment variable before the process starts. `client.New` picks this up. - Call `rt.SetDebug(true)` at runtime. `rt.SetLogger(myLogger)` swaps the destination away from the default standard-library logger. For most production debugging you'll get more value out of the [OpenTelemetry tracing](../tracing/) than from raw dumps. go-openapi-runtime-decad8f/docs/doc-site/usage/core/000077500000000000000000000000001520232310000225505ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/usage/core/_index.md000066400000000000000000000067201520232310000243450ustar00rootroot00000000000000--- title: Core weight: 10 description: | The interfaces, content-type plumbing and validation hooks the client and server pieces are built on. Start here for a mental model. --- The root `github.com/go-openapi/runtime` package defines a small set of interfaces shared by every other piece of the runtime. Everything else — client transport, server middleware pipeline — is built on top of these. ## Concepts * `Producer` and `Consumer` are the _codecs_ that map data structures to and from JSON, YAML, XML, byte streams, etc. * "Parameters bindings" is the machinery to serialize / deserialize OpenAPI request parameters as go types * "Content negotiation" refers to the handling of the `Content-Type` header to agree on a serialization and encoding format. * "Operation" is the OpenAPI term for "handler", i.e. a unitary service invoked by a request ## Module map `runtime` ships as **three Go modules**, each with its own `go.mod` and dependencies. | Module | Purpose | |---------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| | `github.com/go-openapi/runtime` | Core interfaces, content-type codecs, client transport, full server middleware pipeline. Pulls in `analysis`, `loads`, `spec`, `strfmt`, `validate`. | | `github.com/go-openapi/runtime/server-middleware` | Standalone, dependency-free server middleware: media types, content negotiation, doc UIs. Usable from any plain `net/http` server. | | `github.com/go-openapi/runtime/client-middleware/opentracing` | Optional OpenTracing transport middleware (compatibility add-on — new code should use the OpenTelemetry support built into `client.Runtime`). | > `server-middleware` lets you reuse the negotiation > and doc-UI primitives without inheriting the OpenAPI spec/loads/validate > dependency tree. ## Where the pieces fit {{< mermaid align="center" zoom="true" >}} flowchart TD cli["Application client code
(models, …)"] app["Application server code
(handlers, models, …)"] client[["client
(transport)"]] mw[["middleware
(pipeline)"]] sm[["server-middleware
(standalone — stdlib only)"]] core{{"runtime
core interfaces
Consumer · Producer
Authenticator · Authorizer
OperationHandler · Validatable"}} cli -- import --> client app -- import --> mw app --> sm client --> core mw --> core mw -.-> sm {{< /mermaid >}} > `middleware` reuses the `server-middleware` primitives (the dotted > arrow): negotiation, media-type matching and the doc-UI handlers all > live in `server-middleware`. > **Backward-compatibility note** > > The legacy entry points pre-existing in package `middleware` before v0.30.0 (`NegotiateContentType`, `SwaggerUI`, …) > are still available as a shim (`middleware/seam.go`) that now forwards to > the new module — see [server / deprecated shims](../../server/deprecated-shims/). Read on for what each interface does, the built-in content-type codecs and the validation hooks. {{< children type="card" description="true" >}} go-openapi-runtime-decad8f/docs/doc-site/usage/core/content-types.md000066400000000000000000000101211520232310000257010ustar00rootroot00000000000000--- title: Content types weight: 20 description: | Built-in Consumer / Producer factories (JSON, XML, CSV, text, bytestream, YAML) and how to register your own. --- `Consumer` and `Producer` ([interfaces page](../interfaces/)) are the seam through which every wire format plugs in. The runtime ships a handful of built-in factories for the formats most OpenAPI specs use; anything else is a function call away. ## Built-in factories All factories return ready-to-use `Consumer` / `Producer` values. They are side-effect free — call them as many times as you like. | Format | Consumer factory | Producer factory | Common MIME | |----------------------|------------------------------------|------------------------------------|--------------------------------------------------------| | JSON | `runtime.JSONConsumer()` | `runtime.JSONProducer()` | `application/json` | | XML | `runtime.XMLConsumer()` | `runtime.XMLProducer()` | `application/xml`, `text/xml` | | Plain text | `runtime.TextConsumer()` | `runtime.TextProducer()` | `text/plain` | | CSV | `runtime.CSVConsumer(opts…)` | `runtime.CSVProducer(opts…)` | `text/csv` | | Byte stream | `runtime.ByteStreamConsumer(opts…)`| `runtime.ByteStreamProducer(opts…)`| `application/octet-stream`, any unparsed binary type | | YAML | `yamlpc.YAMLConsumer()` | `yamlpc.YAMLProducer()` | `application/yaml`, `application/x-yaml` | YAML lives in a sub-package ([`github.com/go-openapi/runtime/yamlpc`](https://pkg.go.dev/github.com/go-openapi/runtime/yamlpc)) to keep the YAML dependency optional. The CSV and bytestream factories accept option functions (e.g. `runtime.ClosesStream` to make a stream consumer close the underlying reader). See the [godoc](https://pkg.go.dev/github.com/go-openapi/runtime) for the full option list. ## Registering codecs on a server The server side keeps two `map[mediaType]Consumer` / `…Producer` lookups, populated at API construction time. For an untyped API: {{< code file="core/contenttypes/main.go" lang="go" region="registerCodecsServer" >}} Code generated by go-swagger calls these for you, one entry per `consumes` and `produces` value declared in the spec. ## Registering codecs on a client `client.Runtime` exposes per-content-type maps via `Consumers` and `Producers` on the runtime value. Generated clients populate them; for direct use: {{< code file="core/contenttypes/main.go" lang="go" region="registerCodecsClient" >}} ## Writing a custom Consumer / Producer A consumer is a function. The runtime never inspects its concrete type — implementing `Consumer` (or supplying a `ConsumerFunc`) is enough. {{< code file="customcodec/uint32.go" lang="go" options="linenos=table" >}} Register the resulting `Consumer` under whatever MIME types should dispatch to it. ## Selection rules How the runtime chooses *which* consumer / producer to use for a given request — including wildcards, MIME parameters, and the asymmetric matching rule — is documented in [tutorials / media-type selection](../../tutorials/media-types/) and surfaced site-side under [standalone / content negotiation](../../standalone/content-negotiation/). ## Client-side override: `ContentTyper` A request body value can declare its own `Content-Type` by implementing [`runtime.ContentTyper`](https://pkg.go.dev/github.com/go-openapi/runtime#ContentTyper): ```go type ContentTyper interface { ContentType() string } ``` When a body payload set via `SetBodyParam` is a stream and `ContentType()` returns a non-empty value, that value wins over the operation's `consumes` default. Same goes for individual file values inside a multipart upload. The full algorithm is in [tutorials / media-type selection](../../tutorials/media-types/). go-openapi-runtime-decad8f/docs/doc-site/usage/core/interfaces.md000066400000000000000000000167451520232310000252320ustar00rootroot00000000000000--- title: Interfaces & layering weight: 10 description: | The five core interfaces — Consumer, Producer, Authenticator, Authorizer, OperationHandler — and where each one fires on the client and server sides. --- All interfaces live in the root package [`github.com/go-openapi/runtime`](https://pkg.go.dev/github.com/go-openapi/runtime). Each one comes with a companion `*Func` adapter so plain functions can be used wherever an implementation is required. ## The five interfaces ### `Consumer` — bind a request body to a Go value ```go type Consumer interface { Consume(io.Reader, any) error } type ConsumerFunc func(io.Reader, any) error ``` See the authoritative definition in [godoc: `runtime.Consumer`](https://pkg.go.dev/github.com/go-openapi/runtime#Consumer). Used on **both sides**: - **server**: deserialize the inbound request body into the parameter struct matched to the operation - **client**: deserialize the response body into the operation's typed result ### `Producer` — write a Go value to an HTTP response ```go type Producer interface { Produce(io.Writer, any) error } type ProducerFunc func(io.Writer, any) error ``` See the authoritative definition in [godoc: `runtime.Producer`](https://pkg.go.dev/github.com/go-openapi/runtime#Producer). Used on **both sides**: - **server**: serialize the operation handler's return value into the response body - **client**: serialize a request body before sending The split between `Consumer` and `Producer` is deliberate — request deserialization and response serialization are independent concerns and a given content type may want different behaviour on each side (think of streaming uploads vs. buffered downloads). ### `Authenticator` — turn raw auth data into a principal ```go type Authenticator interface { Authenticate(any) (bool, any, error) } type AuthenticatorFunc func(any) (bool, any, error) ``` See the authoritative definition in [godoc: `runtime.Authenticator`](https://pkg.go.dev/github.com/go-openapi/runtime#Authenticator). The three return values mean: | Value | Meaning | |-----------|------------------------------------------------------------------| | `bool` | did this scheme apply to the request? (false ⇒ try the next one) | | `any` | the authenticated principal (whatever your app uses) | | `error` | non-nil ⇒ scheme applied but failed | Server-only. Built-in implementations for Basic, API key, Bearer and OAuth2 live in the [`security`](https://pkg.go.dev/github.com/go-openapi/runtime/security) package, each with a context-aware `*Ctx` variant. ### `Authorizer` — gate the principal for this specific request ```go type Authorizer interface { Authorize(*http.Request, any) error } type AuthorizerFunc func(*http.Request, any) error ``` See the authoritative definition in [godoc: `runtime.Authorizer`](https://pkg.go.dev/github.com/go-openapi/runtime#Authorizer). Authentication answers _who_; authorization answers _may they do this?_. Authorizer runs after a principal has been resolved. A non-nil error blocks the request. Server-only. There is no built-in authorizer — you wire your own. ### `OperationHandler` — your business logic ```go type OperationHandler interface { Handle(any) (any, error) } type OperationHandlerFunc func(any) (any, error) ``` See the authoritative definition in [godoc: `runtime.OperationHandler`](https://pkg.go.dev/github.com/go-openapi/runtime#OperationHandler). Server-only. The argument is the bound (and validated) parameter struct; the return value is whatever `Producer` will then turn into the response body. `error` propagates to the configured error handler. ## Server lifecycle — where each interface fires For a request that reaches a matched route, the conventional pipeline runs the following stages. Each stage is a separate `http.Handler` in the chain, composable via `middleware.Builder`. {{< mermaid align="center" zoom="true" >}} flowchart TD req(((HTTP request))) router["Router
match path/method against spec"] sec["Security · Context.Authorize
Authenticator → principal
Authorizer → may proceed?"] neg["ContentType / Accept negotiation
pick Consumer + target Producer
(part of BindValidRequest)"] bind["Binder
path/query/header/body params
— uses Consumer —"] val["Validator
param validation + Validatable"] op["OperationExecutor
call OperationHandler.Handle"] resp["Responder
— uses Producer —"] out(((HTTP response))) req --> router --> sec --> neg --> bind --> val --> op --> resp --> out {{< /mermaid >}} A few things worth knowing: - **The order above is a convention, not a runtime invariant.** It is what the runtime's untyped path ([`middleware.newRoutableUntypedAPI`](https://pkg.go.dev/github.com/go-openapi/runtime/middleware)) does — it wraps the bind+validate closure with `newSecureAPI` so that security runs first — and what go-swagger's generated typed handlers do (each operation's `ServeHTTP` calls `Context.Authorize` *then* `Context.BindValidRequest` *then* the handler). You can compose a different chain via `middleware.Builder` if you have a reason to. - **Security comes before binding and validation.** That way an unauthenticated request short-circuits with 401 without paying for parameter binding or body deserialization. - **Auth is a single call site, not two.** `Context.Authorize` runs the configured authenticators in order and, on success, calls the optional `Authorizer`. An `Authenticator` returning `(false, nil, nil)` means "this scheme does not apply" and the next one is tried; a non-nil error short-circuits with 401. - The pipeline's stages are documented in detail under [server / pipeline](../../server/pipeline/). ## Client lifecycle — where each interface fires {{< mermaid align="center" zoom="true" >}} flowchart TD gen["generated client method
(GetPet, ListUsers, …)"] cop["ClientOperation
{Params, Reader, …}
(request descriptor)"] submit["Runtime.Submit"] enc["Producer
encode body → *http.Request"] auth["AuthInfoWriter
attach auth headers"] rt["Transport.RoundTrip
(net/http)"] dec["Consumer
decode response body"] res(((typed result))) gen --> cop --> submit submit --> enc --> rt submit --> auth --> rt rt --> dec --> res {{< /mermaid >}} `Authenticator` and `Authorizer` are **not used on the client**. Client-side auth is attached through `AuthInfoWriter`, covered under [client / authentication](../../client/auth/). ## Which interface goes where? | Interface | Server | Client | Notes | |--------------------|:------:|:------:|----------------------------------------------------------------| | `Consumer` | ✓ | ✓ | request body in (server) / response body in (client) | | `Producer` | ✓ | ✓ | response body out (server) / request body out (client) | | `Authenticator` | ✓ | | scheme picks a principal from the request | | `Authorizer` | ✓ | | gates the principal for this request | | `OperationHandler` | ✓ | | your business logic | | `Validatable` /
`ContextValidatable` | ✓ | ✓ | model self-validation; details in [validation](../validation/) | go-openapi-runtime-decad8f/docs/doc-site/usage/core/validation.md000066400000000000000000000066761520232310000252430ustar00rootroot00000000000000--- title: Validation hooks weight: 30 description: | Validatable and ContextValidatable interfaces, when the runtime invokes them, and how they interact with spec-based validation. --- OpenAPI specifies most validation declaratively (required fields, pattern, min/max, enum, etc.). go-swagger turns those rules into code on the generated model types via two interfaces: ```go type Validatable interface { Validate(strfmt.Registry) error } type ContextValidatable interface { ContextValidate(context.Context, strfmt.Registry) error } ``` Both live in the root [`runtime`](https://pkg.go.dev/github.com/go-openapi/runtime) package — see [`runtime.Validatable`](https://pkg.go.dev/github.com/go-openapi/runtime#Validatable) and [`runtime.ContextValidatable`](https://pkg.go.dev/github.com/go-openapi/runtime#ContextValidatable) for the authoritative definitions. The `strfmt.Registry` argument carries the active string-format registry (date-time, UUID, …) so format-aware validation has access to it. `ContextValidatable` is the context-aware version; it should be preferred in new code because some validations (read-only / write-only flags, async-driven cross-field checks) genuinely need request scope. ## When the runtime calls them Server-side, validation runs as part of `Context.BindValidRequest`, which fires **after** [security](../interfaces/#server-lifecycle--where-each-interface-fires) and **just after** parameter binding: {{< mermaid align="center" zoom="true" >}} flowchart TD sec["Security · Context.Authorize
Authenticator → Authorizer"] bind["Binder
Consumer decodes body into the parameter struct"] val["Validator (per parameter)
1. spec-driven validation (required, pattern, …)
2. if Validatable: Validate(formats)
3. if ContextValidatable: ContextValidate(ctx, formats)"] err{{"errors.CompositeValidationError
aggregates every parameter-level violation
(does not stop on first failure)"}} sec --> bind --> val val -. on error .-> err {{< /mermaid >}} Two consequences worth being aware of: - **Multiple errors per parameter set.** A request with three invalid fields produces a `CompositeValidationError` containing three entries, not a single one. - **Both layers run.** Implementing `Validatable` does not turn off spec-driven validation; the two layers compose. Use `Validatable` for rules the spec cannot express (cross-field invariants, business rules). Client-side, generated request models implement the same interfaces, and the generated `Validate` method runs before the body is serialised — a malformed payload fails locally instead of producing a server-side 422. ## Custom validation in your own types Most users never write these by hand — they fall out of `swagger generate`. But for hand-rolled types you can add cross-field checks like this: {{< code file="core/validation/main.go" lang="go" region="dateRangeValidate" >}} For checks that depend on the request: {{< code file="core/validation/main.go" lang="go" region="contextValidate" >}} ## Strfmt registry Both methods take a `strfmt.Registry`, which is how the runtime carries named formats (`date-time`, `uuid`, `email`, …) into the validator. You rarely build one by hand — the server's `*Context` and the client `Runtime` each carry one and pass it down. To register a custom format (`x-go-type` style), call `strfmt.Default.Add(...)` once at startup; the default registry is what both sides use unless overridden. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/000077500000000000000000000000001520232310000234365ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/usage/examples/_index.md000066400000000000000000000014071520232310000252300ustar00rootroot00000000000000--- title: Examples weight: 50 description: | Runnable snippets covering common runtime usage scenarios — server assembly, client setup, custom middleware and authentication. --- Each page below is a self-contained snippet using the **untyped** API setup so the runtime primitives are visible. Typed (go-swagger generated) servers call exactly the same primitives — the wiring file is just generated for you. Where a topic has more material than fits on a page (like authentication), it gets its own subsection. For a fully runnable copy of any of these patterns, the [`go-swagger/examples`](https://github.com/go-swagger/examples) sibling repo has end-to-end programs you can clone and run. ## Subsections {{< children type="card" description="true" depth=1 >}} go-openapi-runtime-decad8f/docs/doc-site/usage/examples/auth/000077500000000000000000000000001520232310000243775ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/usage/examples/auth/_index.md000066400000000000000000000036121520232310000261710ustar00rootroot00000000000000--- title: Authentication & authorization weight: 10 description: | Worked examples covering API keys, HTTP Basic, Bearer/JWT, OAuth2 access-code flow, composed schemes, custom authorizers, and client-side credential attachment. --- OpenAPI 2.0 defines four auth flavours; the runtime covers all four plus the orthogonal `Authorizer` step. The pages below walk one concrete scenario each — the first three cover the simplest cases, the rest progressively layer on scopes, composition and custom business rules. ## When to use which | Situation | Start with | |-----------------------------------------------------------|------------------------------------------------------------------| | Single static API key in a header or query param | [api-key](./api-key/) | | Username:password against a local store | [basic](./basic/) | | OAuth2 / OIDC bearer tokens with scope checks | [bearer-jwt](./bearer-jwt/) | | You actually need the OAuth2 access-code dance with Google | [oauth2-access-code](./oauth2-access-code/) | | Multiple schemes per operation (AND / OR composition) | [composed](./composed/) | | RBAC / per-route business rules over the principal | [custom-authorizer](./custom-authorizer/) | | Client side — attaching credentials to outgoing requests | [client-side](./client-side/) | For the conceptual model (interfaces, lifecycle, where each stage fires), see [server / security](../../server/security/) and [core / interfaces](../../core/interfaces/). {{< children type="card" description="true" >}} go-openapi-runtime-decad8f/docs/doc-site/usage/examples/auth/api-key.md000066400000000000000000000027551520232310000262710ustar00rootroot00000000000000--- title: API key (single scheme) weight: 10 description: | Simplest server-side auth — a single API key carried in a header (or query parameter), validated against a static map. --- The shortest path to a secured endpoint. Mirrors the [`go-swagger/examples/authentication`](https://github.com/go-swagger/examples/tree/master/authentication) example, in untyped form. ## Spec ```yaml securityDefinitions: key: type: apiKey in: header name: X-Token # default: every operation requires the key security: - key: [] ``` ## Wiring {{< code file="auth/apikey/main.go" lang="go" region="wireAPIKeyAuth" >}} The scheme name passed to `RegisterAuth` (`"key"`) must match the key under `securityDefinitions` in the spec. ## Exercise ```sh # Valid token → 200 curl -i -H 'X-Token: abcdefuvwxyz' http://127.0.0.1:35307/customers/42 # Wrong / missing token → 401 curl -i -H 'X-Token: nope' http://127.0.0.1:35307/customers/42 # {"code":401,"message":"invalid api key"} ``` ## Variations - **Query param instead of header**: change `in:` to `query` in the spec and the second arg of `APIKeyAuth` to `"query"`. The token comes from `?api_key=…`. - **Context-aware lookup** (DB call honouring request cancellation): use [`security.APIKeyAuthCtx`](../../../server/security/#why-ctx) instead — same idea, the callback gets the request `context.Context`. - **Per-operation override**: a route can opt out by setting `security: []`; opt into a different scheme by replacing the list. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/auth/basic.md000066400000000000000000000030171520232310000260030ustar00rootroot00000000000000--- title: HTTP Basic weight: 20 description: | RFC 7617 Basic auth with realm and a WWW-Authenticate challenge on failure. --- Same shape as the [API key](../api-key/) example, but with username:password decoded by the runtime and a realm advertised on the failure response. ## Spec ```yaml securityDefinitions: basicAuth: type: basic security: - basicAuth: [] ``` ## Wiring {{< code file="auth/basic/main.go" lang="go" region="registerBasicAuth" >}} `BasicAuthRealmCtx` is the context-aware variant of `BasicAuthRealm`; the non-`*Ctx` form [`security.BasicAuthRealm("petstore", fn)`](https://pkg.go.dev/github.com/go-openapi/runtime/security#BasicAuthRealm) takes a `func(user, pass string) (any, error)` instead. ## Replying with `WWW-Authenticate` on 401 The runtime stashes the realm name in the request context when basic auth has been attempted and failed. Recover it from a custom error handler to render a proper challenge: {{< code file="auth/basic/main.go" lang="go" region="failedBasicAuthChallenge" >}} `FailedBasicAuth(r)` is the non-context spelling. ## Exercise ```sh # Valid credentials curl -i -u alice:s3cret http://127.0.0.1:8080/pets # Missing or wrong credentials → 401 with challenge curl -i http://127.0.0.1:8080/pets # HTTP/1.1 401 Unauthorized # WWW-Authenticate: Basic realm="petstore" ``` ## When to combine with other schemes Basic + Bearer is a common "either credential works" requirement. That's the AND/OR composition case — see [composed](../composed/) for how to declare and wire it. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/auth/bearer-jwt.md000066400000000000000000000055171520232310000267730ustar00rootroot00000000000000--- title: Bearer + JWT weight: 30 description: | OAuth2-style Bearer tokens carrying a JWT — verified locally, scope-checked against the operation's required scopes. --- The runtime extracts the token from `Authorization: Bearer …` (or the `access_token` query / form field — see [server / security](../../../server/security/#bearerauth--oauth2--bearer-tokens)). Your callback verifies it and decides whether the token's claimed scopes satisfy the operation's required scopes. ## Spec OpenAPI 2.0 only declares scopes under `type: oauth2`. Use that declaration even if you're not running an OAuth2 dance — the runtime treats it as "extract a Bearer token and pass me the required scopes". ```yaml securityDefinitions: hasRole: type: oauth2 flow: accessCode authorizationUrl: 'https://issuer.example.com/auth' # documentary only tokenUrl: 'https://issuer.example.com/token' # documentary only scopes: customer: regular customer admin: administrative actions security: - hasRole: [customer] # default: any operation needs at least "customer" ``` ## Wiring JWT parsing is shown here via a `parseJWT` stub so the doc-examples module does not lock you into a specific library — swap it for [`jwt.ParseWithClaims`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#ParseWithClaims) (or an introspection call) in your own code. {{< code file="auth/bearerjwt/main.go" lang="go" region="wireBearerAuth" >}} The first argument to `BearerAuth` is the **scheme name** — match the key under `securityDefinitions`. It is recoverable from the request via `security.OAuth2SchemeName(r)` when an operation declares more than one OAuth2 entry. ## Token sources, in order The runtime tries, in this order: 1. `Authorization: Bearer ` 2. `?access_token=…` query parameter 3. `access_token` form field if `Content-Type` is `application/x-www-form-urlencoded` or `multipart/form-data` That covers RFC 6750 §2. ## Exercise ```sh TOKEN=$(jwt -key keys/private.pem -alg RS256 sign \ -claim 'sub=alice' -claim 'roles=["customer"]') curl -i -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/orders/42 # Or via query param: curl -i "http://127.0.0.1:8080/orders/42?access_token=$TOKEN" ``` ## Variations - **Remote verification (introspection)**: replace the local `jwt.ParseWithClaims` with an HTTP call to your auth server's `/introspect` endpoint. Use `BearerAuthCtx` so the introspection call honours the request context. - **OIDC / Google bearer tokens**: the [oauth2-access-code](../oauth2-access-code/) example shows the full handshake plus the token-validation callback. - **Multiple bearer schemes**: not supported — the runtime extracts one token and passes it to whichever bearer authenticator applies for the route. The [composed](../composed/) example walks the standard workaround. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/auth/client-side.md000066400000000000000000000062421520232310000271250ustar00rootroot00000000000000--- title: Client-side credentials weight: 70 description: | Attaching auth information to outgoing requests — Basic, API key, Bearer, composed writers, and a custom HMAC signer. --- Server-side authentication is the *Authenticator* story. Client-side authentication is the *ClientAuthInfoWriter* story — pure encoding: take credentials, set the right header / query parameter on the outgoing request. See [client / authentication](../../../client/auth/) for the full reference; this page is a recipe collection. ## Built-in writers {{< code file="auth/clientside/main.go" lang="go" region="builtinWriters" >}} `DefaultAuthentication` is used for any operation that does not specify its own. Per-operation override: {{< code file="auth/clientside/main.go" lang="go" region="perOperationOverride" >}} ## Composing multiple credentials For APIs that require more than one credential header on the same request (an API key plus a bearer token, say): {{< code file="auth/clientside/main.go" lang="go" region="composeWriters" >}} `Compose` skips nil writers; the first one to return an error short-circuits the chain. ## Refreshing OAuth2 tokens [`BearerToken`](https://pkg.go.dev/github.com/go-openapi/runtime/client#BearerToken) captures a fixed string. For tokens that need to refresh mid-session, wrap a [`golang.org/x/oauth2.TokenSource`](https://pkg.go.dev/golang.org/x/oauth2#TokenSource): ```go // requires `golang.org/x/oauth2` — left inline because the doc-examples // module intentionally avoids pulling in that dependency. import ( "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" "golang.org/x/oauth2" ) func OAuth2(src oauth2.TokenSource) runtime.ClientAuthInfoWriter { return runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { tok, err := src.Token() // refreshes when expired if err != nil { return err } return r.SetHeaderParam("Authorization", "Bearer "+tok.AccessToken) }) } rt.DefaultAuthentication = OAuth2(myTokenSource) ``` ## Custom: HMAC body signing Sign the body with a shared secret and attach the signature as a header: {{< code file="auth/clientside/main.go" lang="go" region="hmacSignatureWriter" >}} Then wire it on the runtime: ```go rt.DefaultAuthentication = HMACSignature("k1", sharedSecret) ``` The runtime calls `AuthenticateRequest` after the operation's parameters have been bound — so for buffered bodies `r.GetBody()` returns the encoded payload. For streaming bodies (multipart, raw streams) the runtime arranges a body-copy closure so the signer sees the bytes that go on the wire; see [client / requests](../../../client/requests/#what-happens-during-a-submitcontext-call) for the exact assembly path. ## Explicit "no auth" For operations whose spec lists a security requirement that should be satisfied by sending nothing (rare but legal): {{< code file="auth/clientside/main.go" lang="go" region="passThroughAuth" >}} A nil writer would have the same effect — [`PassThroughAuth`](https://pkg.go.dev/github.com/go-openapi/runtime/client#PassThroughAuth) is the explicit version, useful when you want the intent to read clearly in review. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/auth/composed.md000066400000000000000000000076151520232310000265430ustar00rootroot00000000000000--- title: Composed schemes (AND / OR) weight: 40 description: | Multiple security schemes per operation — AND inside an entry, OR between entries — with a single principal type. --- Mirrors the [`go-swagger/examples/composed-auth`](https://github.com/go-swagger/examples/tree/master/composed-auth) example, condensed. That sibling repo has the full runnable code, the JWT helpers, the keypair-generation script and a curl exerciser. ## The composition rule Inside one `security` list entry, all schemes must succeed (**AND**). Between entries, any successful entry wins (**OR**). The runtime stops at the first entry that authenticates. ```yaml security: # OR - isRegistered: [] # entry 1: AND of one scheme hasRole: [customer] - isReseller: [] # entry 2: AND of two schemes hasRole: [inventoryManager] - isResellerQuery: [] # entry 3: alternative carrier hasRole: [inventoryManager] ``` That reads as: *(registered AND customer-scoped)* **OR** *(reseller-by-header AND inventory-manager-scoped)* **OR** *(reseller-by-query AND inventory-manager-scoped)*. ## Spec sketch ```yaml securityDefinitions: isRegistered: # Authorization: Basic … type: basic isReseller: # X-Custom-Key: type: apiKey in: header name: X-Custom-Key isResellerQuery: # ?CustomKeyAsQuery= type: apiKey in: query name: CustomKeyAsQuery hasRole: # Bearer + scopes type: oauth2 flow: accessCode authorizationUrl: 'https://example.com/auth' # documentary tokenUrl: 'https://example.com/token' # documentary scopes: customer: regular customer inventoryManager: reseller managing inventory ``` ## Wiring {{< code file="auth/composed/main.go" lang="go" region="wireComposedAuth" >}} The callbacks (`authenticateBasic`, `verifyResellerToken`, `verifyBearerWithScopes`) each return the same principal type — the runtime hands the principal of the *winning* entry to the operation handler, regardless of which schemes participated. ## One principal, many origins A common consequence of OR composition is that you can't tell from the operation handler alone *which* path authorized the call. Two patterns: - **Annotate inside the callback**: stash the auth flavour on the principal struct (`principal.Source = "basic"` etc.) before returning it. - **Read it back from the request context**: for OAuth2 entries, use [`security.OAuth2SchemeName(r)`](https://pkg.go.dev/github.com/go-openapi/runtime/security#OAuth2SchemeName) to recover the matched scheme name. For Basic, `FailedBasicAuth` reports the realm only on failure. ## Caveats (from the example's own README) - **At most one `Authorization` header.** Mixing `Authorization: Basic` and `Authorization: Bearer` is not supported by HTTP itself; the Bearer carrier should fall back to the `access_token` query/form field when Basic is also in play. - **At most one scoped scheme per route.** If a spec declares two `oauth2` entries, both will see the same Bearer token — the runtime has no way to tell them apart at the wire level. - **OpenAPI 2.0 only allows scopes on `oauth2`.** That's why the example uses `type: oauth2` for what is really plain JWT-with-claims. - **All schemes share one principal type.** Aggregate intermediary state inside the principal struct itself. ## Run it end-to-end The full runnable program — including the JWT keypair generator, a curl exerciser script and the JWT-claims-based authorizers — lives at [`go-swagger/examples/composed-auth`](https://github.com/go-swagger/examples/tree/master/composed-auth). The runtime side of that example is exactly what you see above; the rest is application glue (DB lookups, JWT verification helpers, the RSA keypair) that you'd write the same way against any HTTP framework. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/auth/custom-authorizer.md000066400000000000000000000045211520232310000304270ustar00rootroot00000000000000--- title: Custom Authorizer (RBAC) weight: 60 description: | Pluggable Authorizer that gates the principal for the matched operation — a worked role-based access control example, orthogonal to whichever Authenticator was used. --- Authentication answers *who*. Authorization answers *may they do this?* — a separate decision the runtime asks of your `Authorizer` **after** the principal has been resolved ([core / interfaces](../../../core/interfaces/#server-lifecycle--where-each-interface-fires)). The runtime ships one trivial authorizer (`security.Authorized()` — always-allow). Anything more interesting you write yourself. ## A role-based authorizer {{< code file="auth/customauthorizer/main.go" lang="go" region="rbacAuthorizer" >}} Two things worth knowing about the return value: - A return implementing `errors.Error` is propagated as-is (status code preserved). - Any other error is wrapped as `errors.New(403, err.Error())`. That's why the example uses `errors.New(http.StatusForbidden, …)` rather than `fmt.Errorf` — to keep control of the status code. ## Wire it {{< code file="auth/customauthorizer/main.go" lang="go" region="wireAuthorizer" >}} That's it — the runtime calls `Authorize` on every authenticated request after the authenticator has populated the principal. ## Reading the principal & scopes elsewhere Inside extra middleware mounted via [`middleware.Builder`](../../../server/pipeline/#composing-extra-middleware--builder), or from a custom error handler: {{< code file="auth/customauthorizer/main.go" lang="go" region="readPrincipal" >}} Useful for audit logging, per-tenant rate limiting, or surfacing a "why was this denied?" message in error responses. ## Variations - **OPA / Casbin / your own engine**: same shape — call out to the policy evaluator from inside the `AuthorizerFunc`. - **Skip authorization for some routes**: combine the ACL with a short-circuit on the matched route (`route.Operation.ID`, `route.PathPattern`, etc.) before consulting the engine. - **Per-method body inspection**: `Authorizer` runs after authentication but **before** parameter binding, so the request body has not been consumed at this point — for body-based decisions ("the document the user is editing must belong to them"), do the check inside the operation handler, where the bound params are available. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/auth/oauth2-access-code.md000066400000000000000000000072541520232310000303020ustar00rootroot00000000000000--- title: OAuth2 access-code (Google) weight: 50 description: | Full OAuth2 access-code handshake against Google — login redirect, callback handler, token exchange and protected operations. --- Mirrors [`go-swagger/examples/oauth2`](https://github.com/go-swagger/examples/tree/master/oauth2). Most of this example is OAuth2-flow plumbing (redirect, callback, token exchange) that lives in *your* code, not in the runtime — the runtime only enters the picture for the protected endpoints, where the bearer token is validated. The [bearer-jwt](../bearer-jwt/) example is the right starting point if all you need is *validating* an inbound bearer; come here when you also want to *issue* the redirect dance. ## Spec ```yaml securityDefinitions: OauthSecurity: type: oauth2 flow: accessCode authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth' tokenUrl: 'https://www.googleapis.com/oauth2/v4/token' scopes: user: regular user admin: administrative security: - OauthSecurity: [user] paths: /login: get: security: [] # public — kicks off the redirect /auth/callback: get: security: [] # public — receives the code from Google /customers: get: # uses the default `OauthSecurity: [user]` ... ``` ## Application configuration You'll need a registered OAuth2 client at and an exact-match callback URL. ```go import ( oidc "github.com/coreos/go-oidc" "golang.org/x/oauth2" ) var ( state = "foobar" // single-shot CSRF token; see note below clientID = "" clientSecret = "" callbackURL = "http://127.0.0.1:12345/api/auth/callback" userInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo" config = oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, Endpoint: oauth2.Endpoint{ AuthURL: "https://accounts.google.com/o/oauth2/v2/auth", TokenURL: "https://www.googleapis.com/oauth2/v4/token", }, RedirectURL: callbackURL, Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } ) ``` ## Wiring {{< code file="auth/oauth2/main.go" lang="go" region="wireOauth2AccessCode" >}} `validateAtUserInfoURL` is a plain HTTP call to Google's userinfo endpoint with the bearer token — see the [full implementation](https://github.com/go-swagger/examples/blob/master/oauth2/restapi/implementation.go) in the sibling repo. > **State parameter, briefly**: the example uses a global string for > brevity. In production this MUST be a per-session unguessable > value, stored alongside the user's session and validated on the > callback — otherwise CSRF on the redirect. ## Exercise ```sh # 1. Visit the login URL in a browser open http://127.0.0.1:12345/api/login # → redirected to Google sign-in # → after consent, redirected back to /auth/callback # → the response includes the access_token # 2. Call a protected endpoint with that token curl -i -H "Authorization: Bearer $TOKEN" http://127.0.0.1:12345/api/customers # Wrong token → 401 curl -i -H "Authorization: Bearer garbage" http://127.0.0.1:12345/api/customers # {"code":401,"message":"unauthenticated for invalid credentials"} ``` ## Run the full example The complete runnable program — including the userinfo validator, the redirect/callback handlers wired through middleware, and the client-secret bootstrap — lives at [`go-swagger/examples/oauth2`](https://github.com/go-swagger/examples/tree/master/oauth2). Clone it, drop your Google client ID/secret into `restapi/implementation.go`, and run. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/content-types/000077500000000000000000000000001520232310000262525ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/usage/examples/content-types/_index.md000066400000000000000000000011741520232310000300450ustar00rootroot00000000000000--- title: Content types & negotiation weight: 20 description: | Adding new wire formats, registering vendor MIME types, streaming bodies, per-payload Content-Type overrides, and using the standalone negotiator from a vanilla net/http handler. --- The runtime ships codecs for JSON, XML, CSV, plain text, byte streams and YAML ([core / content-types](../../core/content-types/)). Anything beyond that — a different format, vendor MIME types, large streaming bodies, per-payload Content-Type overrides — is a few lines of glue. The pages below each tackle one such glue case. {{< children type="card" description="true" >}} go-openapi-runtime-decad8f/docs/doc-site/usage/examples/content-types/content-typer.md000066400000000000000000000063701520232310000314150ustar00rootroot00000000000000--- title: Per-payload Content-Type override weight: 40 description: | The runtime.ContentTyper interface — declaring a payload's wire Content-Type from the value itself, on the client side. --- The client normally derives the request `Content-Type` from the operation's `consumes` list. Two cases need an override: - a stream payload (`io.Reader` / `io.ReadCloser` set via `SetBodyParam`) whose actual format isn't what `consumes` defaults to - an individual file part inside a multipart upload that has its own per-part Content-Type (rather than `http.DetectContentType`-sniffed) [`runtime.ContentTyper`](https://pkg.go.dev/github.com/go-openapi/runtime#ContentTyper) is the seam: ```go type ContentTyper interface { ContentType() string } ``` When the runtime picks up a body or file value that satisfies this interface and `ContentType()` returns a non-empty string, **that value wins**. An empty return is treated as "no opinion" and the runtime falls back to its default selection. The full algorithm — the order of precedence and how it interacts with `consumes` and the negotiator — is in [tutorials / media-type selection](../../../tutorials/media-types/). ## Stream payloads — naming the wire format Use this when you're sending a binary blob whose precise format you know, and you want the recipient (or a proxy) to see the right header instead of `application/octet-stream`: {{< code file="contenttypes/contenttyper/main.go" lang="go" region="streamPayload" >}} If `imagePayload` did not implement `ContentType()`, the runtime would use whichever entry in `op.ConsumesMediaTypes` it picked (typically `application/octet-stream`). ## Multipart file parts — per-part Content-Type In a multipart request, individual file values are normally typed via `http.DetectContentType` (sniffed from the first 512 bytes). Implementing `ContentTyper` on the file value bypasses that: {{< code file="contenttypes/contenttyper/main.go" lang="go" region="multipartFileType" >}} ```go // Wiring (illustrative — Params is built by the generated client): f, _ := os.Open("manifest.json") part := taggedFile{File: f, mime: "application/vnd.acme.manifest+json"} // op.Params.SetFileParam("manifest", part) ← part header carries // "Content-Type: application/vnd.acme.manifest+json" ``` Without `ContentType()` the multipart writer would sniff the bytes and likely write `text/plain` or `application/json` — both wrong if your downstream pipeline keys on the vendor type. ## Server-side equivalent? There is none — server responses pick a `Producer` from the `Accept`-negotiated `produces` entry, and the producer writes the response. If you need to influence the response `Content-Type` beyond what `produces` allows, use a custom `middleware.Responder` that sets the header explicitly before delegating to the producer. ## Caveats - `ContentTyper` is **client-side only** for body and multipart-file values. It is not consulted on response payloads. - Implementing it on a value that is *not* one of those two (a regular struct passed as a typed body) has no effect — the operation's `consumes` entry wins. - An empty `ContentType()` return is "no opinion", not "force empty header". The runtime falls back to its default. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/content-types/custom-codec.md000066400000000000000000000057561520232310000311760ustar00rootroot00000000000000--- title: Custom codec (MessagePack) weight: 10 description: | Register a Consumer and Producer for a wire format the runtime does not ship — using MessagePack as the worked example. --- `Consumer` and `Producer` are functions; adding a codec for a new wire format is just writing two of them and registering them under the right MIME type. This page uses [`github.com/vmihailenco/msgpack/v5`](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5) as the worked example because it's the most widely-used Go MessagePack implementation; any third-party codec works the same way. ## Pick a Content-Type MessagePack has no IANA-registered MIME. Two conventions are common: - `application/x-msgpack` (older `x-` style) - `application/msgpack` (newer) Pick one and stick to it across spec, server registration and client expectation. The examples below use `application/x-msgpack`. ## The Consumer + Producer pair {{< code file="contenttypes/customcodec/main.go" lang="go" region="consumerProducerPair" >}} Two-line implementations are typical; the runtime never inspects codec internals. Anything more sophisticated (configurable encoder options, format-specific error wrapping) goes inside the closure. ## Register on the server Spec — declare the new MIME under `consumes` / `produces`: ```yaml consumes: - application/json - application/x-msgpack produces: - application/json - application/x-msgpack ``` Wire it up: {{< code file="contenttypes/customcodec/main.go" lang="go" region="registerOnServer" >}} The runtime now picks MessagePack whenever the inbound `Content-Type` matches and the route lists `application/x-msgpack` under `consumes`, or `Accept: application/x-msgpack` selects it from `produces`. ## Register on the client {{< code file="contenttypes/customcodec/main.go" lang="go" region="registerOnClient" >}} For an individual call, set the operation's content-type lists: {{< code file="contenttypes/customcodec/main.go" lang="go" region="operationMediaTypes" >}} ## Exercise ```sh # Server happily decodes a MessagePack body curl -i -H 'Content-Type: application/x-msgpack' \ --data-binary @payload.msgpack \ http://127.0.0.1:8080/v1/items # And produces MessagePack on request curl -i -H 'Accept: application/x-msgpack' \ http://127.0.0.1:8080/v1/items/42 ``` A request with `Content-Type` outside the operation's `consumes` list yields **415 Unsupported Media Type**; an `Accept` outside `produces` yields **406 Not Acceptable**. See [server / pipeline](../../../server/pipeline/#failure-modes-by-stage) for the full failure-mode mapping. ## Variations - **Vendor MIME types** (`application/vnd.acme.v1+msgpack`) need separate registrations even when they delegate to the same codec — see [vendor types](../vendor-types/). - **Streaming bodies**: `Consumer` / `Producer` get an `io.Reader` / `io.Writer` directly, so streaming codecs work the same way. The [streaming bodies](../streaming-bodies/) page covers raw-byte payloads and the `ClosesStream` option. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/content-types/negotiate-standalone.md000066400000000000000000000042171520232310000327050ustar00rootroot00000000000000--- title: Negotiation in plain net/http weight: 50 description: | Use server-middleware/negotiate from a vanilla net/http handler — no OpenAPI spec, no go-openapi/runtime dependency. --- The [`server-middleware`](../../../standalone/) module ships content negotiation as a standalone, dependency-free package. You can drop it into any `net/http` application — no spec, no analyzer, no `go-openapi/runtime` import. ## Install ```sh go get github.com/go-openapi/runtime/server-middleware ``` The full module pulls only the standard library at runtime (testify is `_test.go`-only). ## Pick a response Content-Type {{< code file="contenttypes/negotiatestandalone/main.go" lang="go" region="pickContentType" >}} `ContentType` returns the most-acceptable offer per the request's `Accept` header (q-values, specificity, position-as-tiebreaker). If no offer is acceptable, the third argument (the *default offer*) is returned. ## Exercise ```sh # JSON by preference curl -i -H 'Accept: application/json' http://127.0.0.1:8080/pet # XML preferred, JSON acceptable curl -i -H 'Accept: application/xml;q=0.9, application/json;q=0.5' \ http://127.0.0.1:8080/pet # Both rejected → falls back to the default offer (application/json here) curl -i -H 'Accept: text/html' http://127.0.0.1:8080/pet ``` ## MIME-parameter behaviour As of v0.30 the negotiator honours MIME parameters by default — an `Accept` of `text/plain;charset=utf-8` does **not** match an offer of `text/plain;charset=ascii`. Pre-v0.30 the parameters were stripped before matching. Opt out per call to restore the old behaviour: {{< code file="contenttypes/negotiatestandalone/main.go" lang="go" region="ignoreParameters" >}} Full algorithm and rationale: [standalone / content negotiation](../../../standalone/content-negotiation/). ## Adding a Swagger UI to the same server The same module ships [`docui`](../../../standalone/doc-ui/) — stdlib-only handlers for Swagger UI / RapiDoc / Redoc. Combining the two gives you a small spec-served, doc-UI-equipped HTTP server with no OpenAPI runtime dependency at all. See [docui standalone](../../docui-standalone/) (queued) once we write that example. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/content-types/streaming-bodies.md000066400000000000000000000056021520232310000320330ustar00rootroot00000000000000--- title: Streaming bodies weight: 30 description: | ByteStreamConsumer and ByteStreamProducer for large up- and downloads — without buffering the whole payload in memory. --- For payloads that are not naturally a single Go value — large file downloads, log streams, raw binary uploads — `runtime.ByteStreamConsumer` and `runtime.ByteStreamProducer` give you `io.Reader` / `io.Writer` access without the runtime decoding into a typed model. ## Server — streaming a download Spec: ```yaml paths: /backups/{id}: get: operationId: GetBackup produces: - application/octet-stream responses: '200': description: backup blob schema: type: string format: binary ``` Wiring: {{< code file="contenttypes/streamingbodies/main.go" lang="go" region="serverDownload" >}} `Produce` accepts an `io.Reader` (yes, despite the name): the default `ByteStreamProducer` copies bytes through. For typed bodies the runtime would marshal first; here you stay in raw-byte territory end to end. ## Server — streaming an upload Spec: ```yaml paths: /backups: post: operationId: PutBackup consumes: - application/octet-stream parameters: - in: body name: blob schema: type: string format: binary responses: {…} ``` Wiring: {{< code file="contenttypes/streamingbodies/main.go" lang="go" region="consumerWithCloses" >}} `ClosesStream` is the option to use when the consumer should `Close()` the underlying reader after consumption. Default is *not* to close — useful when you want to inspect the same body twice or the caller manages the lifetime explicitly. The bound parameter is an `io.ReadCloser`; stream straight to disk: {{< code file="contenttypes/streamingbodies/main.go" lang="go" region="serverUpload" >}} ## Client — sending and receiving streams Build a client request whose body is an `io.Reader` (or `runtime.NamedReadCloser` if you also want a filename for the `Content-Disposition`): {{< code file="contenttypes/streamingbodies/main.go" lang="go" region="clientStream" >}} For multipart uploads with file parts and form fields, the shape differs — see [client multipart](../../client-multipart/) (queued). ## Choosing between ByteStream and a typed Consumer Use `ByteStreamConsumer` / `Producer` when: - the payload is genuinely opaque bytes (downloads, uploads of binary blobs, logs) - the size could exceed RAM — buffered codecs would OOM - you want to forward the body to another service without re-encoding Use a typed Consumer/Producer (JSON, XML, …, [custom codec](../custom-codec/)) when the payload is a structured value the operation handler needs to inspect. The two are not mutually exclusive — a single API can route some operations to streams and others to typed payloads via operation-level `consumes` / `produces`. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/content-types/vendor-types.md000066400000000000000000000056131520232310000312400ustar00rootroot00000000000000--- title: Vendor MIME types weight: 20 description: | Versioning an API through vendor MIME types (application/vnd.acme.v1+json) — separate registrations per type, shared codec. --- API versioning by vendor MIME type — `application/vnd.acme.v1+json` and friends — is a common alternative to `/v1/` URL prefixes. The runtime supports it, but each MIME registers as its own entry: the `+json` structural suffix is **not** sniffed automatically. ## Spec ```yaml consumes: - application/vnd.acme.v1+json - application/vnd.acme.v2+json produces: - application/vnd.acme.v1+json - application/vnd.acme.v2+json ``` ## Server registration Both versions decode the same JSON wire format, so they share the codec. They still need separate registrations: {{< code file="contenttypes/vendortypes/main.go" lang="go" region="registerVendorTypes" >}} `JSONConsumer()` and `JSONProducer()` are side-effect free — calling them per registration is fine. ## Picking the version inside the handler The matched `Content-Type` is on the request context — recover it via `runtime.ContentType(r.Header)`: {{< code file="contenttypes/vendortypes/main.go" lang="go" region="dispatchOnContentType" >}} For the response side, the runtime has already chosen a producer that matches the client's `Accept` — your handler returns a value and the matched `Producer` writes it. If you need the response shape to differ between versions, branch on the negotiated content-type the same way (see `Context.ResponseFormat`). ## Matching rules — what about MIME parameters? The [asymmetric matching rule](../../../standalone/media-types/#the-asymmetric-matching-rule) applies. If your spec lists a parameterised type (`application/vnd.acme+json;version=1`), an inbound request with no `version` parameter does **not** match. Recommend the simpler form — parameter-distinct types are rarely worth the surprise. For the v0.30 parameter-honouring change and the per-call opt-out, see [standalone / content negotiation](../../../standalone/content-negotiation/#behaviour-change-in-v030--mime-parameters-honoured). ## Adding a non-JSON vendor type The exact same shape — register a separate Consumer/Producer per declared MIME, even when several share the same codec: ```go import msgpackcodec "example.com/myapp/codecs/msgpack" api.RegisterConsumer("application/vnd.acme.v1+msgpack", msgpackcodec.Consumer()) api.RegisterProducer("application/vnd.acme.v1+msgpack", msgpackcodec.Producer()) ``` See [custom codec](../custom-codec/) for the `msgpackcodec` package itself. ## When *not* to do this Vendor MIME types compose poorly with browser clients (`Accept: */*` is unspecific), with caches that key on URL alone, and with HTTP middleware that inspects the URL. URL-based versioning (`/v1/...`) sidesteps all three. Pick vendor MIME types when the API is server-to-server *and* you genuinely need the same URL to serve multiple representations. go-openapi-runtime-decad8f/docs/doc-site/usage/examples/middleware/000077500000000000000000000000001520232310000255535ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/usage/examples/middleware/_index.md000066400000000000000000000014521520232310000273450ustar00rootroot00000000000000--- title: Custom middleware weight: 30 description: | Composing third-party HTTP middleware around the runtime — recipes that wrap or extend the `http.Handler` returned by `middleware.Serve`. --- The runtime pipeline (*Router → Security → Bind → Validate → OperationHandler → Responder*) lives behind a single `http.Handler`. Standard ecosystem middleware — compression, logging, rate-limiting, tracing — composes around that handler the usual way. Order matters: transport-level concerns (TLS termination, auth gating, rate limits) typically wrap whatever middleware needs to see the final response bytes (compression, logging), which in turn wraps the runtime pipeline. The pages below cover specific compositions worth pinning down. {{< children type="card" description="true" >}} go-openapi-runtime-decad8f/docs/doc-site/usage/examples/middleware/compression.md000066400000000000000000000054631520232310000304460ustar00rootroot00000000000000--- title: Compression weight: 10 description: | Adding transparent HTTP response compression (gzip, brotli, …) to a runtime server by wrapping the `http.Handler` returned by `middleware.Serve` with the CAFxX `httpcompression` adapter. --- This example shows how to add transparent HTTP response compression (gzip, brotli, …) to a `go-openapi/runtime` server by wrapping the `http.Handler` returned by `middleware.Serve` with a standard ecosystem compression middleware. The runtime itself does not ship compression. Composition with an external middleware is the recommended approach; this example uses [`github.com/CAFxX/httpcompression`](https://github.com/CAFxX/httpcompression), which covers gzip + brotli + zstd + deflate with sensible defaults (content-type allowlist, minimum-size threshold, `Vary` / `ETag` / `Content-Length` handling). ## The wiring The runtime hands you an `http.Handler`. Wrap it with the compression adapter and mount the result on the mux: {{< code file="middleware/compression/main.go" lang="go" region="compressionWiring" >}} `DefaultAdapter()` enables gzip + brotli with sensible defaults. Use `Adapter(...)` for explicit codec, threshold, and content-type control (e.g. `httpcompression.GzipCompressionLevel(6)`, `httpcompression.MinSize(512)`, `httpcompression.ContentTypes([]string{"application/json"}, false)`). ## Run ```sh go run . ``` Then in another terminal: ```sh # Plain (uncompressed) response. curl -i http://localhost:8080/api/greeting # Gzip-compressed response. curl -i -H 'Accept-Encoding: gzip' http://localhost:8080/api/greeting # Brotli-compressed response (DefaultAdapter enables brotli too). curl -i -H 'Accept-Encoding: br' --compressed http://localhost:8080/api/greeting ``` The compressed response carries `Content-Encoding: gzip` (or `br`), `Vary: Accept-Encoding`, and a transformed `Content-Length`. The `go-openapi/runtime` pipeline is unchanged — the compressor sits outside the API handler and operates on the final response bytes. ## Layering The order of middlewares around the api handler matters: ```text client ─► [ TLS / auth / rate-limit ] ─► [ compress ] ─► [ go-openapi api ] ─► handler ``` The compressor must wrap the api handler so it sees the complete response body before transport. Transport-level concerns (TLS termination, auth gating, rate limiting) typically wrap the compressor in turn. ## Client-side `net/http`'s default transport auto-decodes `gzip` responses, but not `br` / `zstd` / `deflate`. Clients that need broader decoding can wrap their `http.RoundTripper` with a decoder; [`github.com/klauspost/compress`](https://github.com/klauspost/compress) provides primitives suitable for that purpose. The `go-openapi/runtime` client (`client.Runtime`) accepts a custom transport via its configuration, so the same pattern applies. go-openapi-runtime-decad8f/docs/doc-site/usage/features/000077500000000000000000000000001520232310000234365ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/usage/features/_index.md000066400000000000000000000230531520232310000252310ustar00rootroot00000000000000--- title: "Features" type: home description: Features and compliance to internet standards. weight: 1 --- A primer on what this runtime implementation supports, with normative references to the standards each feature implements. Citations point at the canonical specification rather than secondary sources. ## Client & Server * **HTTP/1.1** and **HTTP/2** over plaintext or TLS. HTTP/2 is inherited transparently from Go's `net/http` stack on both client and server when ALPN negotiates `h2`; no runtime-specific wiring. See [HTTP core](#http-core) below for the supporting RFCs. * **[Content negotiation][mdn-conneg]** on `Accept` / `Accept-Encoding`, honouring MIME parameters and quality values ([RFC 9110 §12][rfc9110-conneg], [§12.4.2 quality values][rfc9110-q]). * **URI templating** for path-parameter expansion (Level-1 simple expansion only) per [RFC 6570][rfc6570]; values are percent-encoded per [RFC 3986 §2][rfc3986-pe]. * **Structured-suffix MIME** matching — e.g. `application/vnd.acme+json` falls back to the `application/json` codec ([RFC 6838 §4.2.8][rfc6838-suffix], [IANA structured-syntax-suffix registry][iana-suffix]). * **Routing** against an analyzed OpenAPI specification. * **Predefined codecs**: | Format | Reference | |---------------|--------------------------------------------------------------| | JSON | [RFC 8259][rfc8259] | | XML | [W3C XML 1.0][w3c-xml] (via Go `encoding/xml`) | | CSV | [RFC 4180][rfc4180] | | `text/plain` | [RFC 2046 §4.1][rfc2046-text] | | Byte stream | `application/octet-stream` — [RFC 2046 §4.5.1][rfc2046-bin] | | YAML | [YAML 1.2][yaml-1.2] (via the `yamlpc` sub-package) | * **Parameter binding** for every OpenAPI parameter location: * **Path** parameters with URI Template Level-1 expansion ([RFC 6570][rfc6570]). * **Query** parameters. * **Header** parameters. * **Request body** decoded through the matched `Consumer`. * **Streaming bodies**: * **File upload** via `multipart/form-data` ([RFC 7578][rfc7578]) or `application/x-www-form-urlencoded` ([WHATWG URL][whatwg-urlenc]). * Other streams via `application/octet-stream` ([RFC 2046 §4.5.1][rfc2046-bin]) or any custom MIME, surfaced as `io.Reader` / `io.Writer`. ### Trailing-slash behaviour * **Strictly preserved by the client** — the path supplied by the caller is passed through verbatim. * **Ignored by the server** — a route declared as `/pets` matches both `/pets` and `/pets/`. ### Optional, opt-in * **Loosened content negotiation** (`negotiate.WithIgnoreParameters`): * Strip MIME parameters before matching (`application/json; charset=utf-8` → `application/json`). * Match the structured MIME suffix (`application/vnd.acme+json` → `application/json`). ## Client * Configurable HTTP transport, TLS / mTLS ([RFC 8446][rfc8446]), proxy support per [RFC 9110 §7.6.4][rfc9110-via]. * Pluggable authentication writers (see [Authentication](#authentication-schemes)). * Built-in **OpenTelemetry** tracing ([OpenTelemetry spec][otel-spec]); legacy OpenTracing support remains in a sibling compatibility module. * Debug mode — request / response dumping enabled via the `Runtime.Debug` field (or `Runtime.SetDebug(true)`); useful while iterating on a generated client. ## Server * Composable middleware pipeline: *Router → Security → Bind → Validate → OperationHandler → Responder*. * Pluggable error rendering via `api.ServeError`. * Built-in doc-UI middleware: SwaggerUI, RapiDoc, Redoc. ## Authentication schemes The runtime parses the standard auth headers and dispatches to application-supplied callbacks for credential / token validation. Token issuance, JWT signature checking, and OIDC ID-token validation are out of scope — they belong in the callback. * **HTTP Basic** — header parsing per [RFC 7617][rfc7617]. * **API Key** in header, query, or cookie — OpenAPI security scheme convention; no dedicated RFC. * **Bearer** tokens — header parsing per [RFC 6750][rfc6750]. The runtime treats the bearer value as an opaque string; downstream parsing (JWT, opaque tokens, …) is the callback's responsibility. * **OAuth 2.0** — the runtime exposes the same Bearer hook with the OAuth-2 framing ([RFC 6749][rfc6749]; [RFC 8252][rfc8252] for native apps). All four grant flows (authorization code, implicit, client credentials, password) work because the runtime sees only the resulting access token. ## Not supported (yet) * **Language negotiation** — `Accept-Language` / `Content-Language` headers and language-tag parsing. * **Compression** — `Accept-Encoding` / `Content-Encoding` negotiation and the content-coding registry (gzip, Brotli, zstd). * **HTTP caching** — `Cache-Control` / `ETag` / `Last-Modified` / validators. ## Normative references ### OpenAPI specifications * [OpenAPI v2 (Swagger 2.0)][oas2] — the dialect this runtime targets. ### HTTP core * [RFC 9110][rfc9110] — HTTP Semantics (supersedes RFC 7230-7235). * [RFC 9111][rfc9111] — HTTP Caching. * [RFC 9112][rfc9112] — HTTP/1.1. * [RFC 9113][rfc9113] — HTTP/2. * [RFC 8446][rfc8446] — TLS 1.3 · [RFC 5246][rfc5246] — TLS 1.2. ### URIs * [RFC 3986][rfc3986] — URI Generic Syntax. * [RFC 6570][rfc6570] — URI Template. ### Media types * [RFC 6838][rfc6838] — Media Type Specifications and Registration (structured-syntax suffixes in §4.2.8). * [IANA structured-syntax-suffix registry][iana-suffix]. * [RFC 8259][rfc8259] — JSON. * [W3C XML 1.0 (5th ed.)][w3c-xml]. * [RFC 4180][rfc4180] — CSV. * [YAML 1.2][yaml-1.2]. * [RFC 7578][rfc7578] — `multipart/form-data`. * [WHATWG URL — `application/x-www-form-urlencoded`][whatwg-urlenc]. ### Authentication * [RFC 7617][rfc7617] — HTTP Basic. * [RFC 6749][rfc6749] — OAuth 2.0 Authorization Framework. * [RFC 6750][rfc6750] — OAuth 2.0 Bearer Token Usage. * [RFC 8252][rfc8252] — OAuth 2.0 for Native Apps. ### Tracing * [OpenTelemetry specification][otel-spec]. [mdn-conneg]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Content_negotiation [oas2]: https://swagger.io/specification/v2/ [rfc9110]: https://www.rfc-editor.org/rfc/rfc9110 [rfc9110-conneg]: https://www.rfc-editor.org/rfc/rfc9110#section-12 [rfc9110-q]: https://www.rfc-editor.org/rfc/rfc9110#section-12.4.2 [rfc9110-via]: https://www.rfc-editor.org/rfc/rfc9110#section-7.6.4 [rfc9110-lang]: https://www.rfc-editor.org/rfc/rfc9110#section-8.5 [rfc9110-enc]: https://www.rfc-editor.org/rfc/rfc9110#section-8.4 [rfc9111]: https://www.rfc-editor.org/rfc/rfc9111 [rfc9112]: https://www.rfc-editor.org/rfc/rfc9112 [rfc9113]: https://www.rfc-editor.org/rfc/rfc9113 [rfc8446]: https://www.rfc-editor.org/rfc/rfc8446 [rfc5246]: https://www.rfc-editor.org/rfc/rfc5246 [rfc3986]: https://www.rfc-editor.org/rfc/rfc3986 [rfc3986-pe]: https://www.rfc-editor.org/rfc/rfc3986#section-2 [rfc6570]: https://www.rfc-editor.org/rfc/rfc6570 [rfc6838]: https://www.rfc-editor.org/rfc/rfc6838 [rfc6838-suffix]: https://www.rfc-editor.org/rfc/rfc6838#section-4.2.8 [iana-suffix]: https://www.iana.org/assignments/media-type-structured-suffix/media-type-structured-suffix.xhtml [rfc8259]: https://www.rfc-editor.org/rfc/rfc8259 [w3c-xml]: https://www.w3.org/TR/xml/ [rfc4180]: https://www.rfc-editor.org/rfc/rfc4180 [yaml-1.2]: https://yaml.org/spec/1.2.2/ [rfc2046-text]: https://www.rfc-editor.org/rfc/rfc2046#section-4.1 [rfc2046-bin]: https://www.rfc-editor.org/rfc/rfc2046#section-4.5.1 [rfc7578]: https://www.rfc-editor.org/rfc/rfc7578 [whatwg-urlenc]: https://url.spec.whatwg.org/#application/x-www-form-urlencoded [rfc7617]: https://www.rfc-editor.org/rfc/rfc7617 [rfc6749]: https://www.rfc-editor.org/rfc/rfc6749 [rfc6750]: https://www.rfc-editor.org/rfc/rfc6750 [rfc7519]: https://www.rfc-editor.org/rfc/rfc7519 [rfc8252]: https://www.rfc-editor.org/rfc/rfc8252 [otel-spec]: https://opentelemetry.io/docs/specs/otel/ [w3c-trace-context]: https://www.w3.org/TR/trace-context/ [bcp47]: https://www.rfc-editor.org/info/bcp47 [rfc5646]: https://www.rfc-editor.org/rfc/rfc5646 [rfc1952]: https://www.rfc-editor.org/rfc/rfc1952 [rfc7932]: https://www.rfc-editor.org/rfc/rfc7932 [rfc8478]: https://www.rfc-editor.org/rfc/rfc8478 go-openapi-runtime-decad8f/docs/doc-site/usage/server/000077500000000000000000000000001520232310000231265ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/usage/server/_index.md000066400000000000000000000011761520232310000247230ustar00rootroot00000000000000--- title: Server weight: 30 description: | Server-side request lifecycle — routing, parameter binding, validation, security and operation execution. --- The `middleware` package wires an analyzed OpenAPI spec into a working HTTP handler. Requests flow through a chain of stages — by default `Router → Security → ContentType/Accept → Binder → Validator → OperationExecutor → Responder` — composable via `middleware.Builder`. Generated typed APIs assemble an equivalent chain explicitly per operation; either way the runtime does not enforce a single fixed pipeline. {{< children type="card" description="true" >}} go-openapi-runtime-decad8f/docs/doc-site/usage/server/binding-validation.md000066400000000000000000000116621520232310000272200ustar00rootroot00000000000000--- title: Parameter binding & validation weight: 20 description: | How path, query, header and body parameters are bound to Go values and validated against the spec. --- The two stages combined as [`Context.BindValidRequest`](https://pkg.go.dev/github.com/go-openapi/runtime/middleware#Context.BindValidRequest) turn the incoming `*http.Request` into a populated parameter struct and surface every spec-level violation in a single response. ## What gets bound, in what order `BindValidRequest` runs four sub-steps. Any non-recoverable error short-circuits before the binder runs; otherwise binder-level errors are aggregated alongside negotiation errors: 1. **Content-Type validation** — `runtime.HasBody(r)` early-outs for bodyless requests; otherwise `runtime.ContentType(r.Header)` parses the header (a malformed value is a 400) and `validateContentType` matches it against the operation's `consumes` (no match ⇒ 415, match ⇒ pick the registered `Consumer`; missing `Consumer` ⇒ 500). 2. **Response format selection** — `negotiate.ContentType(r, route.Produces, …)` picks the offer that best satisfies `Accept`; `""` ⇒ 406 (`errors.InvalidResponseFormat`). 3. **Parameter binding** — for each declared parameter, the binder reads the right place (path / query / header / formData / body), converts the string(s) to the target Go type and applies any default declared in the spec. 4. **Per-parameter validation** — the spec's declarative rules (`required`, `pattern`, `minLength`, `enum`, `format`, …) plus any `Validatable` / `ContextValidatable` your model implements. All errors collected during binding and validation are returned as one `errors.CompositeValidationError`. The validator does **not** stop on first failure — a request with three problems produces three entries, so callers learn about everything in one round-trip. ## Where each parameter `in:` reads from | `in:` | Source | Notes | |--------------|------------------------------------------------|--------------------------------------------------------------------------------------| | `path` | the matched route's `RouteParams` | Names come from the `{placeholder}` segments. Required by definition (no default). | | `query` | `r.URL.Query()` | Multi-valued: see `collectionFormat` (`csv`, `ssv`, `tsv`, `pipes`, `multi`). | | `header` | `r.Header` | Multi-valued via the same `collectionFormat`s; `multi` repeats the header name. | | `formData` | `r.PostForm` for `application/x-www-form-urlencoded`
or `r.MultipartForm` for `multipart/form-data` | File parts come back as `runtime.File`. | | `body` | `r.Body`, decoded via the chosen `Consumer` | Validation runs against the resulting Go value, including any `Validatable` hook. | The binder is reflection-based for the untyped path; generated code uses the same primitives by calling `Context.BindValidRequest(r, route, &Params)` where `&Params` is the generated parameter struct. ## Validation layers Two layers compose. They are not alternatives. ```text 1. Spec-driven validation ├─ required, pattern, minLength/maxLength ├─ minimum/maximum, multipleOf, exclusive bounds ├─ enum, format (date-time, uuid, email, …) └─ items / minItems / maxItems / uniqueItems 2. Validatable / ContextValidatable ├─ Validate(strfmt.Registry) error (sync) └─ ContextValidate(ctx, strfmt.Registry) error (request-scoped) ``` See [core / validation](../../core/validation/) for the full picture of the hooks; `BindValidRequest` is the call site. ## Where this fits in the pipeline Conventionally **after** security and **before** the operation handler — see [pipeline](../pipeline/) for the diagram and the rationale (failed auth short-circuits with 401 before paying the cost of binding/validation). ## Disabling spec-driven parameter validation If you need to bypass the `parameters` block entirely (typically for test harnesses or proxy layers that re-validate downstream), `Context.SetIgnoreParameters(true)` skips spec-driven parameter validation while leaving the rest of the pipeline intact: {{< code file="server/binding/main.go" lang="go" region="ignoreParameters" >}} `Validatable` / `ContextValidatable` hooks on the model still run. ## Reading the bound parameters from extra middleware Bound parameters are cached in the request context. From middleware mounted via `Builder` you can re-fetch them without re-binding: {{< code file="server/binding/main.go" lang="go" region="readMatchedRoute" >}} `MatchedRouteFrom` plus `SecurityPrincipalFrom` and `SecurityScopesFrom` cover the most common middleware needs (audit logging, per-tenant rate limiting, …). go-openapi-runtime-decad8f/docs/doc-site/usage/server/deprecated-shims.md000066400000000000000000000130301520232310000266660ustar00rootroot00000000000000--- title: Deprecated shims weight: 90 description: | Doc-UI handlers, content negotiation and the header package have moved to the standalone server-middleware module — this page lists the old entry points and shows the migration. --- In v0.30 the server-side helpers that don't actually need any OpenAPI machinery were extracted into the [server-middleware](../../standalone/) module. The old entry points in `middleware` still compile (and forward to the new ones) so existing imports keep building, but they are tagged deprecated and will be removed in a future major release. This page is a cheat-sheet for the migration. New code should target the right-hand column directly. ## Content negotiation | Old (`middleware`) | New (`server-middleware/negotiate`) | |----------------------------------------------------------|--------------------------------------------------------------------| | `middleware.NegotiateOption` | `negotiate.Option` | | `middleware.NegotiateContentType(r, offers, def, opts…)` | `negotiate.ContentType(r, offers, def, opts…)` | | `middleware.NegotiateContentEncoding(r, offers)` | _deprecated, no direct replacement_ — see the [compression recipe](../../examples/middleware/compression/) | | `middleware.WithIgnoreParameters(true)` | `negotiate.WithIgnoreParameters(true)` | Same signatures, same semantics. The deprecated forms in `middleware/seam.go` are thin wrappers that call straight through. {{< code file="server/deprecatedshims/main.go" lang="go" region="negotiateBefore" >}} {{< code file="server/deprecatedshims/main.go" lang="go" region="negotiateAfter" >}} See [standalone / content negotiation](../../standalone/content-negotiation/) for the full surface, including the v0.30 MIME-parameter-honouring default. ## Header parsing | Old (`middleware/header`) | New (`server-middleware/negotiate/header`) | |--------------------------------------|--------------------------------------------------------| | `header.AcceptSpec` | `header.AcceptSpec` (re-export) | | `header.Copy`, `ParseList`, etc. | same names, new path | The shim package ([`middleware/header`](https://pkg.go.dev/github.com/go-openapi/runtime/middleware/header)) re-exports everything via type aliases and forwarding functions, so existing code is binary-compatible. Update imports when convenient. ## Doc UI handlers — `SwaggerUI`, `RapiDoc`, `Redoc` The `middleware` shims preserve the option-struct calling convention. The new `docui` package uses functional options and accepts `(next http.Handler, opts ...Option)`. | Old (`middleware`) | New (`server-middleware/docui`) | |------------------------------------------------|-------------------------------------------------------------| | `middleware.SwaggerUI(opts SwaggerUIOpts, next)` | `docui.SwaggerUI(next, opts ...docui.Option)` | | `middleware.RapiDoc(opts RapiDocOpts, next)` | `docui.RapiDoc(next, opts ...docui.Option)` | | `middleware.Redoc(opts RedocOpts, next)` | `docui.Redoc(next, opts ...docui.Option)` | | `middleware.SwaggerUIOAuth2Callback(opts, next)` | `docui.SwaggerUIOAuth2Callback(next, opts...)` | | `middleware.Spec(basePath, spec, next, opts…)` | `docui.ServeSpec(spec, next, docui.WithSpecPath(...))` | Field-to-option mapping for the `*Opts` structs: | `*Opts` field | `docui` option | |-----------------|---------------------------| | `BasePath` | `WithUIBasePath(s)` | | `Path` | `WithUIPath(s)` | | `SpecURL` | `WithSpecURL(s)` | | `Title` | `WithUITitle(s)` | | `Template` | `WithUITemplate(s)` | | `RapiDocURL` / `RedocURL` / `SwaggerURL` | `WithUIAssetsURL(s)` | | Swagger-UI-specific knobs (`OAuthCallbackURL`, presets, favicons) | `WithSwaggerUIOptions(docui.SwaggerUIOptions{…})` | Migration example: {{< code file="server/deprecatedshims/main.go" lang="go" region="swaggerUIBefore" >}} {{< code file="server/deprecatedshims/main.go" lang="go" region="swaggerUIAfter" >}} Methods on `*Opts` types that were only used to manipulate option structs (e.g. `SwaggerUIOpts.EnsureDefaults`) have been **removed** — they were not load-bearing. See [standalone / doc UIs](../../standalone/doc-ui/) for the full options reference, the middleware-factory shape (`UseSwaggerUI`, etc.) and a complete net/http example. ## Why the split? Two reasons: - **Dependency hygiene.** The doc UI and negotiation helpers don't need any OpenAPI machinery. Pulling them through `middleware` made every consumer transitively depend on `go-openapi/spec`, `go-openapi/loads` and `go-openapi/validate`. The standalone module has zero such transitive deps — handy for a service that only wants to serve a static spec and a Swagger UI from a vanilla `net/http` mux. - **API hygiene.** The new functional options are easier to extend than option-struct fields, and let us keep adding knobs without growing struct surfaces. The deprecated shims paper over the older shape so old code keeps building. The plan is to remove the shims in a future major release. Migrating when convenient is enough — there's no urgency, but there's no reason to keep new code on the old paths either. go-openapi-runtime-decad8f/docs/doc-site/usage/server/pipeline.md000066400000000000000000000271701520232310000252640ustar00rootroot00000000000000--- title: Request pipeline weight: 10 description: | How an inbound HTTP request flows through middleware.Context — from routing to security to binding/validation to operation execution and response writing. --- The [`middleware`](https://pkg.go.dev/github.com/go-openapi/runtime/middleware) package wires an analyzed OpenAPI spec into a working `http.Handler`. Every request goes through the same conventional sequence of stages — covered briefly on [core / interfaces](../../core/interfaces/), and expanded here with the actual call sites. ## The full picture {{< mermaid align="center" zoom="true" >}} flowchart TD req(((HTTP request))) router["Router · NewRouter / Context.RouteInfo
match path/method against the analyzed spec
404 / 405 if no route"] sec["Security · Context.Authorize
RouteAuthenticators.Authenticate
then optional Authorizer
401 / 403 on failure"] bvr["BindValidRequest"] neg["ContentType / Accept negotiation
pick Consumer + target Producer
400 / 415 / 406 on failure"] bind["Binder
path / query / header / body params
— uses Consumer —"] val["Validator
spec rules + Validatable
422 with CompositeValidationError on failure"] op["OperationHandler.Handle
your business logic"] resp["Responder · Context.Respond
— uses Producer —"] out(((HTTP response))) req --> router --> sec --> bvr bvr --> neg --> bind --> val val --> op --> resp --> out {{< /mermaid >}} The middle three stages — negotiation, binding, validation — all live inside the single call `Context.BindValidRequest`. Splitting them out in the diagram makes the failure modes (400, 415, 406, 422) easier to trace. The diagram shows the *typical* sequence — what the runtime's default untyped wiring does and what go-swagger's generated typed handlers do. The actual ordering and composition is an implementation detail of the [RoutableAPI](#the-routableapi-seam) plugged into the `middleware.Context`; a custom one can compose the per-route handler differently. ## The `RoutableAPI` seam The `middleware` package handles routing, negotiation, validation and the high-level lifecycle helpers (`RouteInfo`, `Authorize`, `BindValidRequest`, `Respond`). Everything that has to know about *your* API — the per-operation handler, the registered codecs, the auth schemes — sits behind a single interface: ```go package middleware type RoutableAPI interface { HandlerFor(method, path string) (http.Handler, bool) ServeErrorFor(path string) func(http.ResponseWriter, *http.Request, error) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer ProducersFor(mediaTypes []string) map[string]runtime.Producer AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator Authorizer() runtime.Authorizer Formats() strfmt.Registry DefaultProduces() string DefaultConsumes() string } ``` | Method | The runtime asks for… | |----------------------------|---------------------------------------------------------------------------| | `HandlerFor` | the `http.Handler` that runs the per-operation pipeline for this route | | `ServeErrorFor` | the error-rendering function for a given path (defaults to the API's) | | `ConsumersFor` | a `mediaType → Consumer` map for the given list (route's `consumes`) | | `ProducersFor` | a `mediaType → Producer` map for the given list (route's `produces`) | | `AuthenticatorsFor` | a `scheme name → Authenticator` map for the security schemes in scope | | `Authorizer` | the optional `Authorizer` to gate the principal post-authentication | | `Formats` | the `strfmt.Registry` used by validation | | `DefaultProduces` / `DefaultConsumes` | the API-level defaults to fall back to when the route is unspecified | The router calls `HandlerFor(method, path)` once per matched route and serves whatever it gets back. **What that handler does is entirely up to the implementation** — the `RoutableAPI` decides how the bind/validate/security/operation/respond steps are composed. ### Constructors that take a custom `RoutableAPI` {{< code file="server/pipeline/main.go" lang="go" region="contextConstructors" >}} Use `NewRoutableContext` when you have your own implementation (typically the one go-swagger generates for typed APIs, but any type satisfying the interface works). Reach for `NewRoutableContextWithAnalyzedSpec` if you have already produced an `*analysis.Spec` and want to avoid the second analysis pass. ### Two implementations the runtime sees in practice The runtime ships **one** `RoutableAPI` implementation — `routableUntypedAPI`, internal to the `middleware` package. It wraps [`untyped.API`](https://pkg.go.dev/github.com/go-openapi/runtime/middleware/untyped#API) and is what `middleware.Serve` / `ServeWithBuilder` builds for you. go-swagger generates a **second** implementation per spec — the `*operations.MyAPI` type implements every method on `RoutableAPI` directly, with `HandlerFor` returning the per-operation `ServeHTTP` shown below. The next section walks both. ## Two assembly paths The two `RoutableAPI` implementations introduced above produce equivalent pipelines, but differ in *where* the per-route handler is assembled — the untyped one builds it in the runtime via a closure; the typed one is generated source you can read directly. ### Untyped — `middleware.Serve` / `ServeWithBuilder` {{< code file="server/pipeline/main.go" lang="go" region="untypedServer" >}} Internally `middleware.newRoutableUntypedAPI` builds one `http.Handler` per route. The bind/validate/handle/respond logic lives in a single closure; if the route declares any security requirement, that closure is wrapped with `newSecureAPI` so security runs first: ```go // excerpt from middleware/context.go var handler http.Handler = http.HandlerFunc(func(w, r) { bound, r, validation = context.BindAndValidate(r, route) if validation != nil { context.Respond(...); return } result, err := oh.Handle(bound) // … }) if len(schemes) > 0 { handler = newSecureAPI(context, handler) // ← wraps with Authorize } ``` ### Typed — generated `ServeHTTP` per operation go-swagger generates a small handler per operation that calls the same primitives in the same order, but spelt out explicitly: ```go // excerpt from a go-swagger generated *operation.ServeHTTP func (o *GetOrder) ServeHTTP(rw http.ResponseWriter, r *http.Request) { route, rCtx, _ := o.Context.RouteInfo(r) if rCtx != nil { *r = *rCtx } var Params = NewGetOrderParams() uprinc, aCtx, err := o.Context.Authorize(r, route) // ← security if err != nil { o.Context.Respond(rw, r, route.Produces, route, err); return } // … if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // ← bind+validate o.Context.Respond(rw, r, route.Produces, route, err) return } res := o.Handler.Handle(Params, principal) // ← operation o.Context.Respond(rw, r, route.Produces, route, res) // ← respond } ``` Same primitives, same order. **Neither shape is enforced by the runtime**: a route is just an `http.Handler`, and you can wrap or replace it. `middleware.Builder` exists precisely to compose your own chain on top. ## Composing extra middleware — `Builder` ```go type Builder func(http.Handler) http.Handler ``` `Builder` is the standard `http.Handler` decorator type, aliased so the API reads cleanly. The runtime exposes several entry points that take one: | Entry point | Purpose | |------------------------------------------------------|-------------------------------------------------------------------------| | `middleware.Serve(spec, api)` | Untyped, no extra middleware (uses `PassthroughBuilder`). | | `middleware.ServeWithBuilder(spec, api, builder)` | Untyped, decorate the routes handler with `builder`. | | `Context.APIHandler(builder, opts…)` | Mounts the routes plus the default Swagger UI / spec serve middleware. | | `Context.APIHandlerWithUI(builder, ui, opts…)` | Same, but pick the UI flavour (`docui.SwaggerUI` / `RapiDoc` / `Redoc`).| | `Context.RoutesHandler(builder)` | Just the routes — no UI middleware. Useful when you mount under your own mux. | A typical pattern with the [`justinas/alice`](https://github.com/justinas/alice) middleware library — log, rate-limit, then hand off to the runtime: {{< code file="server/pipeline/main.go" lang="go" region="aliceComposition" >}} `PassthroughBuilder` is the identity decorator if you need a place to start. ## Failure modes by stage | Stage | Status | Surfaced as | |--------------------|--------|--------------------------------------------------------------------------------------------------------------------------------| | Router | 404 | `errors.NotFound` | | Router | 405 | `errors.MethodNotAllowed` (with `Allow` header) | | Security | 401 | `errors.Unauthenticated` ("invalid credentials") | | Security | 403 | `errors.New(403, …)` if the `Authorizer` returns a non-`errors.Error` | | Negotiation | 400 | malformed `Content-Type` ⇒ wrapped `errors.ParseError` | | Negotiation | 415 | `errors.InvalidContentType` (no `consumes` entry matches) | | Negotiation | 406 | `errors.InvalidResponseFormat` (no `produces` entry satisfies `Accept`) | | Binding/Validation | 422 | `errors.CompositeValidationError` aggregating every parameter-level violation (does not stop on first failure) | | Operation | varies | whatever the handler returns (`error` ⇒ runs through `Context.Respond` and the API's `ServeError`) | For the matching algorithm and the v0.30 parameter-honouring change behind 415/406 outcomes, see [standalone / content negotiation](../../standalone/content-negotiation/) and the canonical [tutorials / media-type selection](../../tutorials/media-types/). ## Reading values out of the request Each stage stashes its result in the request context so downstream middleware can read it without re-doing the work: | Helper | Returns | |-----------------------------------------------------------------|----------------------------------------| | `middleware.MatchedRouteFrom(r) *MatchedRoute` | the route matched by the router | | `middleware.SecurityPrincipalFrom(r) any` | the principal returned by `Authorize` | | `middleware.SecurityScopesFrom(r) []string` | the union of scopes for the matched scheme | Use these inside extra middleware mounted via `Builder`. go-openapi-runtime-decad8f/docs/doc-site/usage/server/security.md000066400000000000000000000157231520232310000253270ustar00rootroot00000000000000--- title: Security schemes weight: 30 description: | Server-side authenticator implementations — Basic, API key, Bearer, OAuth2 — and their context-aware *Ctx variants. --- [`security`](https://pkg.go.dev/github.com/go-openapi/runtime/security) ships ready-made `runtime.Authenticator` implementations for the four auth flavours OpenAPI 2.0 understands. Each comes in two shapes — a plain variant and a `*Ctx` variant that threads `context.Context` through to your authenticate function. ## The user-supplied callback You don't implement `Authenticator` directly — you implement a verification callback and pass it to one of the constructors below. The runtime handles the wire-format details (header parsing, scheme selection, scope handling, etc.). | Constructor | Your callback signature | |---------------------------------------------------|------------------------------------------------------------------------------------------| | `BasicAuth(fn)` / `BasicAuthRealm(realm, fn)` | `func(user, password string) (principal any, err error)` | | `BasicAuthCtx(fn)` / `BasicAuthRealmCtx(…)` | `func(ctx, user, password) (ctx, principal, err)` | | `APIKeyAuth(name, in, fn)` | `func(token string) (principal, err)` | | `APIKeyAuthCtx(name, in, fn)` | `func(ctx, token) (ctx, principal, err)` | | `BearerAuth(name, fn)` *(OAuth2)* | `func(token string, scopes []string) (principal, err)` | | `BearerAuthCtx(name, fn)` *(OAuth2)* | `func(ctx, token, scopes) (ctx, principal, err)` | A successful callback returns the authenticated principal — typed however your application likes. The principal is then handed to any configured `Authorizer` and stashed in the request context (read with `middleware.SecurityPrincipalFrom`). ## Why `*Ctx`? Most real authenticators want request scope: a request-scoped database handle, a tracing span, or a deadline that should propagate into the auth lookup. The `*Ctx` constructors give your callback the request context and let it return a (possibly enriched) context that the runtime then attaches to the request. {{< code file="server/security/main.go" lang="go" region="basicAuthCtx" >}} The non-`*Ctx` variants exist for compatibility with code from before context propagation was the norm. New code should default to `*Ctx`. ## `BasicAuth` — RFC 7617 {{< code file="server/security/main.go" lang="go" region="basicAuthSimple" >}} `BasicAuth` reads `r.BasicAuth()` and calls your callback with the decoded credentials. Use `BasicAuthRealm("my-realm", fn)` to set the challenge realm advertised in `WWW-Authenticate` on failure (default: `"Basic Realm"`). When the request has no `Authorization` header, the authenticator returns `(false, nil, nil)` — "scheme does not apply" — so the next configured scheme is tried. A non-nil error from your callback is treated as a 401. `security.FailedBasicAuth(r)` / `FailedBasicAuthCtx(ctx)` returns the realm name when basic auth has been attempted and failed. Useful from custom error handlers that want to render a `WWW-Authenticate` challenge. ## `APIKeyAuth` — header or query {{< code file="server/security/main.go" lang="go" region="apiKeyAuthHeader" >}} `in` must be `"header"` or `"query"` — anything else **panics** at construction time (it is a programmer error). The callback receives the raw token; an empty value short-circuits with `(false, nil, nil)` so other schemes can apply. ## `BearerAuth` — OAuth2 / Bearer tokens {{< code file="server/security/main.go" lang="go" region="bearerAuthScopes" >}} The runtime extracts the token from, in order: 1. `Authorization: Bearer ` 2. The `access_token` query parameter 3. The `access_token` form field if `Content-Type` is `application/x-www-form-urlencoded` or `multipart/form-data` That covers RFC 6750 §2. `requiredScopes` is whatever the operation declared in its `security:` block. Combine multiple security entries (per the spec) and you'll see the union or intersection per call — `RouteAuthenticator.AllScopes()` and `CommonScopes()` expose those if you need to inspect them yourself. The "scheme name" you pass (`"oauth2"` here) is recoverable from the request via `security.OAuth2SchemeName(r)` / `security.OAuth2SchemeNameCtx(ctx)`. That's the hook point for code that needs to know *which* OAuth2 entry was applied (handy when a spec declares multiple OAuth2 flows). ## Authorizer Authentication says *who*; authorization says *may they do this?*. Authorizer runs after a principal has been resolved. ```go type Authorizer interface { Authorize(*http.Request, any) error } ``` (see [`runtime.Authorizer`](https://pkg.go.dev/github.com/go-openapi/runtime#Authorizer)) The package ships one trivial implementation: {{< code file="server/security/main.go" lang="go" region="registerAuthorized" >}} Anything more interesting (RBAC, ABAC, OPA / casbin / your own…) you write yourself. A non-nil return blocks the request: - A return value implementing `errors.Error` is propagated as-is. - Any other error is wrapped as `errors.New(403, err.Error())`. The single `Authorize` call on `Context` ([core / interfaces](../../core/interfaces/#server-lifecycle--where-each-interface-fires)) runs `Authenticator` and `Authorizer` in sequence — `Authorizer` only runs if the authenticator returned a principal. ## Composing schemes — `RouteAuthenticators` A spec can declare multiple security requirements per operation. The runtime turns each one into a `RouteAuthenticator` and groups them into `RouteAuthenticators`. `RouteAuthenticators.Authenticate` walks the list and: - returns the first one that returned `(true, principal, nil)`; - collects errors from any that applied but failed (last one wins for the response status); - returns `AllowsAnonymous() == true` if no scheme was required — in that case the request proceeds without a principal. You don't construct `RouteAuthenticators` directly — the runtime builds them from your registered `Authenticator`s (typed APIs do this in generated code; untyped APIs via `untyped.API.AddAuth` and related). The grouping and short-circuit semantics are worth knowing about when you wonder why "scheme A is rejecting and scheme B never runs": that's by design — the first applicable scheme decides. ## Reading the principal back Inside your operation handler, the typed signature gives you the principal directly. From extra middleware mounted via `Builder`: {{< code file="server/security/main.go" lang="go" region="readPrincipal" >}} `scopes` is the `AllScopes()` of the matching `RouteAuthenticator` — useful for audit logging that needs to record which token (or token shape) authorised the request. go-openapi-runtime-decad8f/docs/doc-site/usage/standalone/000077500000000000000000000000001520232310000237505ustar00rootroot00000000000000go-openapi-runtime-decad8f/docs/doc-site/usage/standalone/_index.md000066400000000000000000000046251520232310000255470ustar00rootroot00000000000000--- title: Standalone middleware weight: 40 description: | The dependency-free server-middleware module — media types, content negotiation and doc-UI handlers, usable from any net/http server. --- `github.com/go-openapi/runtime/server-middleware` is a separate Go module that ships the negotiation, media-type and doc-UI primitives without inheriting the OpenAPI spec / loads / validate dependency tree. Drop it into any vanilla `net/http` application. ## Install ```sh go get github.com/go-openapi/runtime/server-middleware ``` ## What's in it | Package | Use it for | |--------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`mediatype`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/mediatype) | Parsed RFC 7231 media-type values, `Set` lists and asymmetric matching — the building block both `negotiate` and the runtime's own server pipeline use. | | [`negotiate`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/negotiate) | Server-side selection from `Accept` via `ContentType`. Honours MIME parameters by default; opt out with `WithIgnoreParameters`. | | [`negotiate/header`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/negotiate/header) | Low-level RFC 7231 header parsing primitives reused by `negotiate`. Use it directly if you need raw `Accept`/`Accept-Encoding` parsing without the typed media-type layer. | | [`docui`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/docui) | Stdlib-only handlers that serve Swagger UI, RapiDoc or Redoc, plus the spec document itself. Mountable on any `net/http` mux. | The module has zero transitive dependencies on `go-openapi/spec`, `go-openapi/loads`, `go-openapi/validate`, or even on the rest of `go-openapi/runtime`. Standard library only. {{< children type="card" description="true" >}} go-openapi-runtime-decad8f/docs/doc-site/usage/standalone/content-negotiation.md000066400000000000000000000066101520232310000302650ustar00rootroot00000000000000--- title: Content negotiation weight: 20 description: | Accept and Accept-Encoding selection via the negotiate package, including the new MIME-parameter-aware default. --- [`server-middleware/negotiate`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/negotiate) sits on top of [`mediatype`](../media-types/) and exposes two single-purpose helpers — one for `Accept`, one for `Accept-Encoding`. ## `ContentType` — pick a response media type ```go func ContentType( r *http.Request, offers []string, defaultOffer string, opts ...Option, ) string ``` Returns the offer most acceptable to the request's `Accept` header. If two offers match with equal weight, the more specific offer wins (`text/*` trumps `*/*`; `type/subtype` trumps `type/*`); after that the earlier entry in `offers` wins. If no offer is acceptable, `defaultOffer` is returned. {{< code file="standalone/contentnegotiation/main.go" lang="go" region="pickContentType" >}} When `Accept` is absent entirely, the **first offer** is returned unchanged. ### Behaviour change in v0.30 — MIME parameters honoured Pre-v0.30 the negotiator stripped MIME-type parameters before matching: an `Accept` of `text/plain;charset=utf-8` matched an offer of `text/plain;charset=ascii` (the charset was thrown away). That was expedient but wrong; v0.30 honours parameters by default: - `Accept: text/plain;charset=utf-8` matches an offer of bare `text/plain` (offer carries no constraint — receiver-side params, [asymmetric rule](../media-types/#the-asymmetric-matching-rule)). - `Accept: text/plain;charset=utf-8` does **not** match an offer of `text/plain;charset=ascii` (charset values disagree). If your producers and `Accept` clients use mismatched charset or version params that you treat as informational, opt out per call — {{< code file="standalone/contentnegotiation/main.go" lang="go" region="ignoreParameters" >}} — or server-wide via the runtime's `middleware.Context`: {{< code file="standalone/contentnegotiation/main.go" lang="go" region="serverWideIgnoreParameters" >}} ## `Accept-Encoding` — not handled here `negotiate.ContentEncoding` is **deprecated**. The runtime does not ship response compression, and surfacing a half-feature negotiator without a matching encoder leads to subtle correctness traps (no `Vary`, no `Content-Length` rewrite, no minimum-size guard). Use a real compression middleware at the `http.Handler` level — see the [compression recipe](../../examples/middleware/compression/) for a worked example using [`CAFxX/httpcompression`](https://github.com/CAFxX/httpcompression). ## Direct header parsing If you only need raw header parsing without the typed `MediaType` layer (for example when implementing a different selection rule), drop down to [`negotiate/header`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/negotiate/header): {{< code file="standalone/contentnegotiation/main.go" lang="go" region="parseAcceptHeader" >}} ## Where it sits in the runtime pipeline The full server pipeline calls `ContentType` (and the matching `Content-Type` validation through `mediatype.MatchFirst`) inside `Context.BindValidRequest`; see [core / interfaces](../../core/interfaces/#server-lifecycle--where-each-interface-fires). The standalone module exposes the same primitives so you can drive negotiation from any `net/http` handler, with or without an OpenAPI spec in the picture. go-openapi-runtime-decad8f/docs/doc-site/usage/standalone/doc-ui.md000066400000000000000000000140741520232310000254600ustar00rootroot00000000000000--- title: Doc UIs & spec serving weight: 30 description: | Stdlib-only Swagger UI, RapiDoc, Redoc and spec-serving handlers from the docui package. --- [`server-middleware/docui`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/docui) ships ready-to-mount `http.Handler`s that serve the three popular OpenAPI documentation UIs and the spec document itself. Standard library only — no template engine, no asset bundler, no transitive OpenAPI dependency. ## Two equivalent patterns Each UI is exposed in two shapes; pick whichever fits your wiring style. ### Direct handler wrap — `SwaggerUI(next, opts...)` For when you already have an `http.Handler` you want to decorate. {{< code file="standalone/docui/main.go" lang="go" region="directWrap" >}} Requests under the configured doc path render the UI; everything else falls through to `next`. ### Middleware factory — `UseSwaggerUI(opts...)` For composition with other middlewares (`alice.New(...)`, your own chain, etc.): {{< code file="standalone/docui/main.go" lang="go" region="middlewareFactory" >}} `Use*` returns a `func(http.Handler) http.Handler` — the standard go-style middleware adapter. ## Available UIs | UI | Direct | Middleware factory | |-------------------------|---------------------------|-----------------------------| | Swagger UI | `docui.SwaggerUI` | `docui.UseSwaggerUI` | | Swagger UI OAuth2 cb | `docui.SwaggerUIOAuth2Callback` | `docui.UseSwaggerUIOAuth2Callback` | | RapiDoc | `docui.RapiDoc` | `docui.UseRapiDoc` | | Redoc | `docui.Redoc` | `docui.UseRedoc` | The OAuth2 callback handler is the small static page Swagger UI redirects to after an OAuth2 authorization — mount it at the path you configure in your OAuth provider. ## Common options | Option | Purpose | Default | |-------------------------------|------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `WithUIBasePath(string)` | Base path the UI is served from. Slash is prepended if missing. | `/` | | `WithUIPath(string)` | Sub-path under the base path (final URL: `{base}/{path}`). | `docs` | | `WithUITitle(string)` | HTML `` of the rendered page. | `API documentation` | | `WithUIAssetsURL(string)` | URL of the JS bundle for the UI. | Redoc → `https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js`<br/>RapiDoc → `https://unpkg.com/rapidoc/dist/rapidoc-min.js`<br/>Swagger UI → `https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js` | | `WithUITemplate(tpl)` | Replace the bundled HTML template entirely (`~string` or `~[]byte`). | bundled minimal template | | `WithSpecURL(string)` | URL the UI fetches the spec from. | `/swagger.json` | | `WithSwaggerUIOptions(opts)` | Pass-through for Swagger-UI-specific knobs (OAuth2 client id, layout, …). | zero value | `WithUITemplate` panics at request time if the supplied template fails to parse or execute — fail loud, not silent. Reference docs for the templates each UI accepts: - Redoc: <https://github.com/Redocly/redoc/blob/main/docs/deployment/html.md> - RapiDoc: <https://github.com/rapi-doc/RapiDoc> - Swagger UI: <https://github.com/swagger-api/swagger-ui> ## Serving the spec document — `ServeSpec` / `UseSpec` The UIs only render — they do not host the spec document themselves. Use the `Spec` helpers for that: {{< code file="standalone/docui/main.go" lang="go" region="serveSpec" >}} or as middleware: {{< code file="standalone/docui/main.go" lang="go" region="useSpec" >}} If you want the spec path the UIs use to stay in sync with the path the spec is served from: {{< code file="standalone/docui/main.go" lang="go" region="pathFromOptions" >}} ## Putting it together A complete net/http server with no OpenAPI runtime in the picture: {{< code file="standalone/docui/main.go" lang="go" region="puttingItTogether" >}} Visit: - `http://localhost:8080/docs` — Swagger UI - `http://localhost:8080/openapi.yaml` — the spec document - `http://localhost:8080/v1/ping` — the application ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/doc-site/usage/standalone/media-types.md����������������������������0000664�0000000�0000000�00000013012�15202323100�0026510�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Media types weight: 10 description: | Typed RFC 7231 media-type values, sets and asymmetric matching via the mediatype package. --- [`server-middleware/mediatype`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/mediatype) provides the parsed value type, the matching rule and the helper used by both server-side `Content-Type` validation and `Accept`-header negotiation. ## The `MediaType` value ```go type MediaType struct { Type string // lowercased on parse Subtype string // lowercased on parse Params map[string]string // keys lowercased; values verbatim Q float64 // extracted from "q="; not stored in Params } ``` See [`MediaType`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/mediatype#MediaType) on pkg.go.dev for the authoritative definition. [`Parse`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/mediatype#Parse) accepts a single media type: {{< code file="standalone/mediatypes/main.go" lang="go" region="parseMediaType" >}} Parameter **values** are preserved verbatim, but comparisons are case-insensitive (`charset=UTF-8` matches `charset=utf-8`). Wildcards `*/*` and `type/*` are accepted on either side; `*/subtype` is invalid and `Parse` rejects it. ### Specificity `MediaType.Specificity()` returns one of the constants below — useful when writing custom selection logic: | Constant | Example | |------------------------------|-------------------------------| | `SpecificityAny` | `*/*` | | `SpecificityType` | `text/*` | | `SpecificityExact` | `text/plain` | | `SpecificityExactWithParams` | `text/plain;charset=utf-8` | ## The asymmetric matching rule `MediaType.Matches(other)` is **asymmetric**. The receiver is the *bound* (an allowed entry on the server side, or a candidate offer when matching against an `Accept` entry); the argument is the *constraint* (the actual request value, or the `Accept` entry being satisfied). The rule: 1. Bare `type/subtype` must agree (with wildcards on either side). 2. If the receiver carries **no parameters**, any constraint is accepted regardless of its parameters. 3. Otherwise every `(key, value)` pair on the constraint must be present on the receiver, with case-insensitive value comparison. The receiver may carry additional parameters that the constraint does not list. q-values are not considered by `Matches` — they belong to the negotiator (see [`Set.BestMatch`](#sets-and-bestmatch)). The same direction is used in both call sites in the runtime: | Call | Bound (receiver) | Constraint (argument) | |-----------------------|--------------------------|--------------------------| | Inbound validation | each entry in `consumes` | request's `Content-Type` | | `Accept` negotiation | each candidate offer | each `Accept` entry | ## `MatchFirst` — the validation primitive ```go func MatchFirst(allowed []string, actual string) (MediaType, bool, error) ``` See [`MatchFirst`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/mediatype#MatchFirst) on pkg.go.dev for the authoritative signature. Used when you need a yes/no answer plus the matched bound. Short-circuits on the first allowed entry that accepts `actual` (so the returned `MediaType` is **not** necessarily the most specific match — use `Set.BestMatch` if you need ranked selection). | Return | Meaning | |------------------------------|-----------------------------------------------------------------------------------------------| | `(matched, true, nil)` | first allowed entry that accepts `actual` | | `(zero, false, nil)` | `actual` is well-formed but no allowed entry accepts it (HTTP 415 territory) | | `(zero, false, err)` | `actual` failed to parse; `err` wraps `ErrMalformed` (HTTP 400 territory — `errors.Is` it) | Allowed entries that themselves fail to parse are skipped silently (they cannot match a well-formed actual). ## Sets and `BestMatch` ```go type Set []MediaType func ParseAccept(s string) Set func (s Set) BestMatch(offered Set) (best MediaType, ok bool) ``` See [`Set`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/mediatype#Set), [`ParseAccept`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/mediatype#ParseAccept) and [`Set.BestMatch`](https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware/mediatype#Set.BestMatch) on pkg.go.dev for the authoritative signatures. `ParseAccept` parses a comma-separated list (e.g. an `Accept` header value), skipping malformed entries silently — be liberal in what you accept. `BestMatch` ranks the *offered* set against the receiver `Accept` set: 1. Highest q-value wins. 2. Ties on q broken by the highest `Specificity` of the matching `Accept` entry. 3. Ties on specificity broken by earliest position in `offered`. Accept entries with `q=0` are treated as **exclusions** and never match. Returns `ok=false` if no offer matched any non-zero-q entry. For the full algorithm — including how `negotiate` wires this up and the v0.30 parameter-honouring change — see [tutorials / media-type selection](../../tutorials/media-types/) and the next page, [content negotiation](../content-negotiation/). ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/�����������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0020623�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0021564�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/apikey/�����������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023046�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/apikey/main.go����������������������������������������0000664�0000000�0000000�00000004171�15202323100�0024324�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command apikey backs the snippets on the doc-site // "API key (single scheme)" example page. The `wireAPIKeyAuth` // region below is the source for the `{{< code region="..." >}}` // include; the package as a whole compiles and lints so the // snippet cannot rot silently. // // `go run .` is a no-op (the demo is built around a blocking HTTP // listener). Pass `serve` to actually start the server on :35307. package main import ( "crypto/subtle" "log" "net/http" "os" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/runtime/security" ) func main() { if len(os.Args) > 1 && os.Args[1] == "serve" { wireAPIKeyAuth() } } // --- Snippets ------------------------------------------------------- func wireAPIKeyAuth() { // snippet:wireAPIKeyAuth doc, _ := loads.Spec("swagger.yml") api := untyped.NewAPI(doc).WithJSONDefaults() // 1. Authenticator: token → principal api.RegisterAuth("key", security.APIKeyAuth( "X-Token", "header", func(token string) (any, error) { // Use subtle.ConstantTimeCompare to avoid leaking the // expected token byte-by-byte via response timing. if subtle.ConstantTimeCompare([]byte(token), []byte("abcdefuvwxyz")) == 1 { return "alice", nil } return nil, errors.New(http.StatusUnauthorized, "invalid api key") }, )) // 2. Authorizer: every authenticated principal allowed. // (Skip this line if you have no business-rule gating.) api.RegisterAuthorizer(security.Authorized()) // 3. Operation handlers (one per spec operation) api.RegisterOperation("get", "/customers/{id}", runtime.OperationHandlerFunc( func(_ any) (any, error) { // params is the bound parameter struct; // principal is on r.Context() via middleware.SecurityPrincipalFrom return map[string]string{"id": "42"}, nil }, )) handler := middleware.Serve(doc, api) log.Fatal(http.ListenAndServe(":35307", handler)) //nolint:gosec // demo handler, no timeouts needed // endsnippet:wireAPIKeyAuth } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/basic/������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0022645�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/basic/main.go�����������������������������������������0000664�0000000�0000000�00000005440�15202323100�0024123�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command basic backs the snippets on the doc-site // "HTTP Basic" auth recipe page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos (wiring registration). package main import ( "context" "crypto/subtle" "net/http" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/runtime/security" ) // --- Stubs (excluded from rendered snippets) ------------------------ // doc is a placeholder OpenAPI document. Snippets pretend it was loaded // from disk; the demo wires a freshly-constructed empty spec so the // program compiles and runs. var doc *loads.Document // fakePrincipal stands in for whatever the application returns from // authentication (e.g. *models.Principal). type fakePrincipal struct{ Name string } // fakeStore stands in for an application-supplied user store. type fakeStore struct{} func (fakeStore) AuthenticateBasic(_ context.Context, user, pass string) (*fakePrincipal, error) { // subtle.ConstantTimeCompare avoids leaking the expected password // byte-by-byte via response timing. The username is non-secret and // compared with `==` purely to short-circuit unknown accounts. if user == "alice" && subtle.ConstantTimeCompare([]byte(pass), []byte("s3cret")) == 1 { return &fakePrincipal{Name: user}, nil } return nil, errors.Unauthenticated("basic") } var store = fakeStore{} // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { registerBasicAuth() failedBasicAuthChallenge() } // --- Snippets ------------------------------------------------------- func registerBasicAuth() { // snippet:registerBasicAuth api := untyped.NewAPI(doc).WithJSONDefaults() api.RegisterAuth("basicAuth", security.BasicAuthRealmCtx( "petstore", func(ctx context.Context, user, pass string) (context.Context, any, error) { // request-scoped lookup — honours ctx cancellation principal, err := store.AuthenticateBasic(ctx, user, pass) if err != nil { return ctx, nil, errors.Unauthenticated("basic") } return ctx, principal, nil }, )) // endsnippet:registerBasicAuth use(api) } func failedBasicAuthChallenge() { api := untyped.NewAPI(doc).WithJSONDefaults() // snippet:failedBasicAuthChallenge api.ServeError = func(w http.ResponseWriter, r *http.Request, err error) { if realm := security.FailedBasicAuthCtx(r.Context()); realm != "" { w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) } errors.ServeError(w, r, err) } // endsnippet:failedBasicAuthChallenge use(api) } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/bearerjwt/��������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023551�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/bearerjwt/main.go�������������������������������������0000664�0000000�0000000�00000005564�15202323100�0025036�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command bearerjwt backs the snippets on the doc-site // "Bearer + JWT" recipe page (usage/examples/auth/bearer-jwt.md). // // The wiring below shows how to register a Bearer authenticator on an // untyped API and intersect the JWT's claimed roles with the operation's // required scopes. JWT parsing is stubbed (`parseJWT`) so the example // does not pull a specific JWT library into the doc-examples module; // swap it for `jwt.ParseWithClaims` from `github.com/golang-jwt/jwt/v5` // (or any other parser) in your own code. // // `go run .` exercises the demo wiring against a no-op spec. package main import ( "net/http" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/runtime/security" ) // --- Stubs (excluded from rendered snippets) ------------------------ // doc stands in for a real `*loads.Document` loaded via `loads.Spec`. var doc *loads.Document // principal is what the authenticator returns on success. The runtime // stores it in the request context for the operation handler to use. type principal struct { Subject string Roles []string } // roleClaims is the subset of JWT claims the example cares about. // In real code this would embed `jwt.RegisteredClaims`. type roleClaims struct { Subject string Roles []string } // parseJWT is a stand-in for a real JWT parser. Replace with // `jwt.ParseWithClaims(token, &roleClaims{}, keyFn)` (or your introspection // call) in production code. func parseJWT(_ string) (*roleClaims, error) { return &roleClaims{}, nil } // intersect returns the elements present in both slices. A real // implementation would normalise case and dedupe. func intersect(a, b []string) []string { set := make(map[string]struct{}, len(b)) for _, s := range b { set[s] = struct{}{} } out := make([]string, 0, len(a)) for _, s := range a { if _, ok := set[s]; ok { out = append(out, s) } } return out } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { wireBearerAuth() } // --- Snippets ------------------------------------------------------- func wireBearerAuth() { // snippet:wireBearerAuth api := untyped.NewAPI(doc).WithJSONDefaults() api.RegisterAuth("hasRole", security.BearerAuth("hasRole", func(token string, requiredScopes []string) (any, error) { claims, err := parseJWT(token) if err != nil { return nil, errors.Unauthenticated("bearer") } // intersect claimed roles with required scopes granted := intersect(claims.Roles, requiredScopes) if len(granted) == 0 { return nil, errors.New(http.StatusForbidden, "insufficient_scope") } return &principal{ Subject: claims.Subject, Roles: granted, }, nil }, )) // endsnippet:wireBearerAuth use(api) } ��������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/clientside/�������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023707�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/clientside/main.go������������������������������������0000664�0000000�0000000�00000006374�15202323100�0025174�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command clientside backs the snippets on the doc-site // "Client-side credentials" recipe page. Each function below is the // source of a `{{< code region="..." >}}` include; the package as a // whole compiles and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos (writer construction and // per-operation wiring). package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" ) // --- Stubs (excluded from rendered snippets) ------------------------ // apiKey, accessToken, operationSpecificToken and sharedSecret stand in // for real credentials. In a production client they would come from // configuration or a secret store. var ( apiKey = "k-stub" accessToken = "t-stub" operationSpecificToken = "op-stub" sharedSecret = []byte("hmac-stub") ) // op stands in for a *runtime.ClientOperation built by generated code. // The snippets only touch its AuthInfo field. var op = &runtime.ClientOperation{} // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { builtinWriters() perOperationOverride() composeWriters() hmacSignatureWriter() passThroughAuth() } // --- Snippets ------------------------------------------------------- func builtinWriters() { // snippet:builtinWriters rt := client.New("api.example.com", "/v1", []string{"https"}) //nolint:goconst // doc example // One of: rt.DefaultAuthentication = client.BasicAuth("alice", "s3cret") rt.DefaultAuthentication = client.APIKeyAuth("X-Api-Key", "header", apiKey) rt.DefaultAuthentication = client.APIKeyAuth("api_key", "query", apiKey) rt.DefaultAuthentication = client.BearerToken(accessToken) // endsnippet:builtinWriters use(rt) } func perOperationOverride() { // snippet:perOperationOverride op.AuthInfo = client.BearerToken(operationSpecificToken) // endsnippet:perOperationOverride } func composeWriters() { rt := client.New("api.example.com", "/v1", []string{"https"}) // snippet:composeWriters rt.DefaultAuthentication = client.Compose( client.APIKeyAuth("X-Api-Key", "header", apiKey), client.BearerToken(accessToken), ) // endsnippet:composeWriters use(rt) } // snippet:hmacSignatureWriter // HMACSignature attaches an HMAC-SHA256 signature of the request body // as `X-Sig`, along with the key identifier as `X-Sig-Key`. func HMACSignature(keyID string, key []byte) runtime.ClientAuthInfoWriter { return runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { body := r.GetBody() mac := hmac.New(sha256.New, key) mac.Write(body) sig := hex.EncodeToString(mac.Sum(nil)) if err := r.SetHeaderParam("X-Sig-Key", keyID); err != nil { return err } return r.SetHeaderParam("X-Sig", sig) }) } // endsnippet:hmacSignatureWriter func hmacSignatureWriter() { rt := client.New("api.example.com", "/v1", []string{"https"}) rt.DefaultAuthentication = HMACSignature("k1", sharedSecret) use(rt) } func passThroughAuth() { // snippet:passThroughAuth op.AuthInfo = client.PassThroughAuth // endsnippet:passThroughAuth } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/composed/���������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023375�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/composed/main.go��������������������������������������0000664�0000000�0000000�00000006244�15202323100�0024656�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command composed backs the snippets on the doc-site // "Composed schemes (AND / OR)" recipe page // (usage/examples/auth/composed.md). // // The wiring below shows how to register multiple authenticators on a // single untyped API so that the spec can compose them with AND (inside // one security entry) and OR (between entries). All callbacks return the // same principal type so the operation handler need not branch on which // scheme matched. The JWT and database lookups are stubbed; swap them // for your own helpers in production code. // // `go run .` exercises the demo wiring against a no-op spec. package main import ( "context" "net/http" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/runtime/security" ) // --- Stubs (excluded from rendered snippets) ------------------------ // doc stands in for a real `*loads.Document` loaded via `loads.Spec`. var doc *loads.Document // principal is the common type returned by every authenticator. The // runtime hands the principal of the *winning* security entry to the // operation handler regardless of which schemes participated, so all // callbacks must agree on this type. type principal struct { Subject string Roles []string // Source records which scheme produced this principal, useful when // the operation handler wants to branch on the auth flavour. Source string } // authenticateBasic stands in for a real user-store lookup behind the // `isRegistered` Basic scheme. func authenticateBasic(ctx context.Context, _, _ string) (context.Context, any, error) { return ctx, &principal{Source: "basic"}, nil } // verifyResellerToken stands in for the JWT parser/validator used by // both `isReseller` (header carrier) and `isResellerQuery` (query // carrier). func verifyResellerToken(_ string) (any, error) { return &principal{Source: "reseller"}, nil } // verifyBearerWithScopes stands in for the OAuth2 bearer validator that // also enforces the operation's required scopes. func verifyBearerWithScopes(_ string, requiredScopes []string) (any, error) { if len(requiredScopes) == 0 { return nil, errors.New(http.StatusForbidden, "insufficient_scope") } return &principal{Source: "bearer", Roles: requiredScopes}, nil } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { wireComposedAuth() } // --- Snippets ------------------------------------------------------- func wireComposedAuth() { // snippet:wireComposedAuth api := untyped.NewAPI(doc).WithJSONDefaults() // One callback per scheme. api.RegisterAuth("isRegistered", security.BasicAuthCtx(authenticateBasic)) api.RegisterAuth("isReseller", security.APIKeyAuth("X-Custom-Key", "header", verifyResellerToken)) api.RegisterAuth("isResellerQuery", security.APIKeyAuth("CustomKeyAsQuery", "query", verifyResellerToken)) api.RegisterAuth("hasRole", security.BearerAuth("hasRole", verifyBearerWithScopes)) api.RegisterAuthorizer(security.Authorized()) // gating happens inside the authenticators // endsnippet:wireComposedAuth use(api) } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/customauthorizer/�������������������������������������0000775�0000000�0000000�00000000000�15202323100�0025213�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/customauthorizer/main.go������������������������������0000664�0000000�0000000�00000006147�15202323100�0026476�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command customauthorizer backs the snippets on the doc-site // "Custom Authorizer (RBAC)" recipe page. Each function below is the // source of a `{{< code region="..." >}}` include; the package as a // whole compiles and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos (wiring registration). package main import ( "fmt" "net/http" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/runtime/security" ) // --- Stubs (excluded from rendered snippets) ------------------------ // doc is a placeholder OpenAPI document. Snippets pretend it was loaded // from disk; the demo wires a freshly-constructed empty spec so the // program compiles. var doc *loads.Document // verifyBearer is a placeholder ScopedTokenAuthentication callback. A // real implementation would validate the bearer token and return a // principal (here a *principal carrying roles). var verifyBearer security.ScopedTokenAuthentication = func(_ string, _ []string) (any, error) { return &principal{Subject: "alice", Roles: []string{"reader"}}, nil } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { _ = RBACAuthorizer() wireAuthorizer() readPrincipal(&http.Request{}) } // --- Snippets ------------------------------------------------------- // snippet:rbacAuthorizer type principal struct { Subject string Roles []string } // roleACL: which roles may call which "method path". var roleACL = map[string]map[string]bool{ "GET /pets": {"reader": true, "admin": true}, //nolint:goconst // doc example "POST /pets": {"writer": true, "admin": true}, "DELETE /pets/{id}": {"admin": true}, } func RBACAuthorizer() runtime.Authorizer { return runtime.AuthorizerFunc(func(r *http.Request, p any) error { route := middleware.MatchedRouteFrom(r) key := fmt.Sprintf("%s %s", r.Method, route.PathPattern) allowed, ok := roleACL[key] if !ok { return errors.New(http.StatusForbidden, "no ACL entry for %s", key) } prin, ok := p.(*principal) if !ok { return errors.New(http.StatusForbidden, "principal type mismatch") } for _, role := range prin.Roles { if allowed[role] { return nil // 👍 } } return errors.New(http.StatusForbidden, "role %v cannot %s", prin.Roles, key) }) } // endsnippet:rbacAuthorizer func wireAuthorizer() { // snippet:wireAuthorizer api := untyped.NewAPI(doc).WithJSONDefaults() api.RegisterAuth("bearer", security.BearerAuth("bearer", verifyBearer)) api.RegisterAuthorizer(RBACAuthorizer()) // endsnippet:wireAuthorizer use(api) } func readPrincipal(r *http.Request) { // snippet:readPrincipal principal := middleware.SecurityPrincipalFrom(r) // any scopes := middleware.SecurityScopesFrom(r) // []string route := middleware.MatchedRouteFrom(r) // *MatchedRoute // endsnippet:readPrincipal use(principal, scopes, route) } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/oauth2/�����������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0022766�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/auth/oauth2/main.go����������������������������������������0000664�0000000�0000000�00000012371�15202323100�0024245�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command oauth2 backs the snippets on the doc-site // "OAuth2 access-code (Google)" recipe page // (usage/examples/auth/oauth2-access-code.md). // // The runtime only enters the OAuth2 access-code dance on the // protected-route side: a Bearer authenticator validates the inbound // access token via an introspection helper. The /login and // /auth/callback handlers are plain redirect/exchange handlers that // live in user code. // // External dependencies on `golang.org/x/oauth2` and `coreos/go-oidc` // are deliberately not imported here — they are illustrative of *user* // wiring, not of the runtime API. The OAuth2 client and userinfo // validator are stubbed at package level so the snippet itself shows // real runtime calls without bloating the doc-examples module with // auth provider SDKs. // // `go run .` exercises the demo wiring against a no-op spec. package main import ( "context" "net/http" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/runtime/security" ) // --- Stubs (excluded from rendered snippets) ------------------------ // doc stands in for a real `*loads.Document` loaded via `loads.Spec`. var doc *loads.Document // state is the single-shot CSRF token shown for brevity in the // recipe. In production this MUST be a per-session unguessable value. var state = "foobar" // oauth2Token mirrors the shape of `*oauth2.Token` used by the snippet // without pulling `golang.org/x/oauth2` into the doc-examples module. type oauth2Token struct { AccessToken string } // oauth2Config mirrors the slice of `oauth2.Config` the snippet calls // into: `AuthCodeURL(state)` for the redirect and `Exchange(ctx, code)` // for the token swap. The real type lives in `golang.org/x/oauth2`. type oauth2Config struct { ClientID string ClientSecret string RedirectURL string } func (oauth2Config) AuthCodeURL(_ string) string { return "" } func (oauth2Config) Exchange(_ context.Context, _ string) (*oauth2Token, error) { return &oauth2Token{}, nil } // config is the application's OAuth2 client config. See the inline // "Application configuration" block in the recipe for the real // `oauth2.Config` literal — it is left fenced (not migrated) because // it is pure external-library wiring. var config oauth2Config // validateAtUserInfoURL is a stand-in for the plain HTTP call to the // provider's userinfo endpoint with the bearer token. Replace with a // real userinfo fetch in production code. func validateAtUserInfoURL(_ string) (bool, error) { return true, nil } // currentRequest stands in for the inbound *http.Request that // `middleware.ResponderFunc` does not expose. In real generated code // the handler receives bound params (with `HTTPRequest`); the recipe // inlines the redirect call to keep the snippet short. var currentRequest *http.Request // getCallbackParams mirrors the bound params struct go-swagger // generates for the `/auth/callback` operation. The untyped flow // exposes the raw HTTP request through the same `HTTPRequest` field. type getCallbackParams struct { HTTPRequest *http.Request } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { wireOauth2AccessCode() } // --- Snippets ------------------------------------------------------- func wireOauth2AccessCode() { // snippet:wireOauth2AccessCode api := untyped.NewAPI(doc).WithJSONDefaults() // 1. The protected-route validator. Called for any operation whose // `security:` block lists `OauthSecurity`. api.RegisterAuth("OauthSecurity", security.BearerAuth("OauthSecurity", func(token string, _ []string) (any, error) { ok, err := validateAtUserInfoURL(token) if err != nil || !ok { return nil, errors.Unauthenticated("bearer") } return token, nil }, )) // 2. /login redirects the browser to Google's auth endpoint. api.RegisterOperation("get", "/login", runtime.OperationHandlerFunc( func(_ any) (any, error) { return middleware.ResponderFunc(func(w http.ResponseWriter, _ runtime.Producer) { // params.HTTPRequest is unused here — pass nil since middleware.Redirect ignores it http.Redirect(w, currentRequest, config.AuthCodeURL(state), http.StatusFound) }), nil }, )) // 3. /auth/callback exchanges the code Google returns for an access token. api.RegisterOperation("get", "/auth/callback", runtime.OperationHandlerFunc( func(params any) (any, error) { // The bound params struct exposes the raw HTTP request through HTTPRequest. // Untyped: extract from params; typed: it's already a field. callbackParams, ok := params.(getCallbackParams) if !ok { panic("internal error") } r := callbackParams.HTTPRequest if r.URL.Query().Get("state") != state { return nil, errors.New(http.StatusBadRequest, "state mismatch") } token, err := config.Exchange(r.Context(), r.URL.Query().Get("code")) if err != nil { return nil, errors.New(http.StatusInternalServerError, "token exchange failed") } return map[string]string{"access_token": token.AccessToken}, nil }, )) // endsnippet:wireOauth2AccessCode use(api) } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/client/����������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0022101�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/client/auth/�����������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023042�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/client/auth/main.go����������������������������������������0000664�0000000�0000000�00000006567�15202323100�0024333�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command auth backs the snippets on the doc-site // "Client / Authentication" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos (wiring registration). package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" ) // --- Stubs (excluded from rendered snippets) ------------------------ // rt is a placeholder client runtime. Snippets pretend it was built // via client.New(...); the demo wires a fresh empty runtime so the // program compiles and runs. var rt = client.New("example.com", "/", []string{"https"}) // op is a placeholder per-operation client request descriptor. var op = &runtime.ClientOperation{} // token, accessToken and apiKey stand in for opaque credentials read // from configuration or a secret manager. const ( token = "demo-token" accessToken = "demo-access-token" apiKey = "demo-api-key" //nolint:gosec // doc example fixture ) // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { attachAuth() basicAuth() apiKeyAuth() bearerAuth() composeAuth() passThroughAuth() useHMAC() } // --- Snippets ------------------------------------------------------- func attachAuth() { // snippet:attachAuth // 1. Per operation — overrides the runtime default op.AuthInfo = client.BearerToken(token) // 2. Per runtime — used when the operation does not set its own rt.DefaultAuthentication = client.BasicAuth("alice", "s3cret") // endsnippet:attachAuth } func basicAuth() { // snippet:basicAuth rt.DefaultAuthentication = client.BasicAuth("alice", "s3cret") // endsnippet:basicAuth } func apiKeyAuth() { // snippet:apiKeyAuth // As an HTTP header rt.DefaultAuthentication = client.APIKeyAuth("X-Api-Key", "header", apiKey) // Or as a query parameter rt.DefaultAuthentication = client.APIKeyAuth("api_key", "query", apiKey) // endsnippet:apiKeyAuth } func bearerAuth() { // snippet:bearerAuth rt.DefaultAuthentication = client.BearerToken(accessToken) // endsnippet:bearerAuth } func composeAuth() { // snippet:composeAuth rt.DefaultAuthentication = client.Compose( client.APIKeyAuth("X-Api-Key", "header", apiKey), client.BearerToken(accessToken), ) // endsnippet:composeAuth } func passThroughAuth() { // snippet:passThroughAuth op.AuthInfo = client.PassThroughAuth // endsnippet:passThroughAuth } func useHMAC() { rt.DefaultAuthentication = HMACSignature("key-1", []byte("shared-secret")) use(rt) } // snippet:hmacSignature // HMACSignature returns a ClientAuthInfoWriter that signs the request // body with the given HMAC-SHA256 key and attaches the signature plus // key ID as headers. func HMACSignature(keyID string, key []byte) runtime.ClientAuthInfoWriter { return runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { body := r.GetBody() mac := hmac.New(sha256.New, key) mac.Write(body) sig := hex.EncodeToString(mac.Sum(nil)) if err := r.SetHeaderParam("X-Sig-Key", keyID); err != nil { return err } return r.SetHeaderParam("X-Sig", sig) }) } // endsnippet:hmacSignature �����������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/client/intro/����������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023234�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/client/intro/main.go���������������������������������������0000664�0000000�0000000�00000002767�15202323100�0024523�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command intro backs the snippet on the doc-site "Client" landing page. // The function below is the source of a `{{< code region="..." >}}` // include; the package as a whole compiles and lints so the snippet // cannot rot silently. // // `go run .` exercises the demo (the SubmitContext call is expected to // fail against the placeholder host — wiring is what we demonstrate). package main import ( "context" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" ) // --- Stubs (excluded from rendered snippets) ------------------------ // token stands in for an OAuth2 / bearer token sourced from the caller's // secret store. var token = "demo-token" // op stands in for a *runtime.ClientOperation that go-swagger-generated // clients build for each operation. The zero value is enough to make // the snippet compile; it will not produce a working HTTP request. var op = &runtime.ClientOperation{} // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { minimalClient(context.Background()) } // --- Snippets ------------------------------------------------------- func minimalClient(ctx context.Context) { // snippet:minimalClient rt := client.New("api.example.com", "/v1", []string{"https"}) rt.DefaultAuthentication = client.BearerToken(token) result, err := rt.SubmitContext(ctx, op) // endsnippet:minimalClient use(rt, result, err) } ���������go-openapi-runtime-decad8f/docs/examples/client/requests/�������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023754�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/client/requests/main.go������������������������������������0000664�0000000�0000000�00000005615�15202323100�0025236�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command requests backs the snippets on the doc-site // "Building & submitting requests" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos. The snippets here construct // requests against a fake host — nothing is actually sent over the wire. package main import ( "context" "net/http" "time" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" ) // --- Stubs (excluded from rendered snippets) ------------------------ // rt is a placeholder client transport. Snippets pretend it was configured // against a real host; the demo wires a freshly-constructed Runtime so the // program compiles and runs. var rt = client.New("example.invalid", "/", []string{"https"}) // op is a placeholder operation descriptor. Snippets pretend it was built // by a generated client; the demo wires a zero-valued descriptor with the // minimum required fields so the program compiles and runs. var op = &runtime.ClientOperation{ ID: "demo", Method: http.MethodGet, PathPattern: "/", Reader: runtime.ClientResponseReaderFunc(func(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return struct{}{}, nil }), } // parent is a placeholder parent context. var parent = context.Background() // myClient is a placeholder http client used by the build-only snippet. var myClient = http.DefaultClient // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} const exampleTimeout = 5 * time.Second func main() { submitVariants() resp, err := createHTTPRequest() if resp != nil { defer resp.Body.Close() } use(resp, err) migrationForm() } // --- Snippets ------------------------------------------------------- func submitVariants() { // snippet:submitVariants // legacy — cached context, hard to cancel from the call site result, err := rt.Submit(op) use(result, err) // preferred — explicit context ctx, cancel := context.WithTimeout(parent, exampleTimeout) defer cancel() result, err = rt.SubmitContext(ctx, op) // endsnippet:submitVariants use(result, err) } func createHTTPRequest() (*http.Response, error) { ctx := context.Background() // snippet:createHTTPRequestContext req, cancel, err := rt.CreateHTTPRequestContext(ctx, op) if err != nil { return nil, err } defer cancel() // MUST run after the response is fully read resp, err := myClient.Do(req) // ... // endsnippet:createHTTPRequestContext return resp, err } func migrationForm() { ctx := context.Background() // snippet:migrationForm // before op.Context = ctx result, err := rt.Submit(op) use(result, err) // after result, err = rt.SubmitContext(ctx, op) // endsnippet:migrationForm use(result, err) } �������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/client/tracing/��������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023530�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/client/tracing/main.go�������������������������������������0000664�0000000�0000000�00000005267�15202323100�0025015�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command tracing backs the snippets on the doc-site // "Client / Tracing" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos (wiring registration — // no real HTTP traffic is made). package main import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" ) const samplePetID = 42 // --- Stubs (excluded from rendered snippets) ------------------------ // petclient is a stand-in for a go-swagger-generated client package // (e.g. example.com/petstore/client). It is declared in this file so the // snippet renders as written without inventing an import path that does // not exist in the example module. var petclient = fakePetClientPkg{} type fakePetClientPkg struct{} // New mimics the generated petclient.New(transport, formats) entry point. func (fakePetClientPkg) New(_ runtime.ClientTransport, _ strfmt.Registry) *fakePetClient { return &fakePetClient{} } // NewGetPetParams mimics the generated params constructor. func (fakePetClientPkg) NewGetPetParams() *fakeGetPetParams { return &fakeGetPetParams{} } type fakePetClient struct { Operations fakeOperations } type fakeOperations struct{} func (fakeOperations) GetPet(_ *fakeGetPetParams) (any, error) { return struct{}{}, nil } type fakeGetPetParams struct{ ID int64 } func (p *fakeGetPetParams) WithID(id int64) *fakeGetPetParams { p.ID = id; return p } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { wireOpenTelemetry() customSpanFormatter() } // --- Snippets ------------------------------------------------------- func wireOpenTelemetry() { // snippet:wireOpenTelemetry rt := client.New("api.example.com", "/v1", []string{"https"}) traced := rt.WithOpenTelemetry() api := petclient.New(traced, strfmt.Default) result, err := api.Operations.GetPet(petclient.NewGetPetParams().WithID(samplePetID)) // endsnippet:wireOpenTelemetry use(traced, api, result, err) } func customSpanFormatter() { rt := client.New("api.example.com", "/v1", []string{"https"}) // snippet:customSpanFormatter traced := rt.WithOpenTelemetry( client.WithSpanNameFormatter(func(op *runtime.ClientOperation) string { return "petstore." + op.ID }), client.WithSpanOptions( trace.WithAttributes( attribute.String("service.name", "petstore-client"), ), ), ) // endsnippet:customSpanFormatter use(traced) } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/client/transport/������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0024135�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/client/transport/main.go�����������������������������������0000664�0000000�0000000�00000006371�15202323100�0025417�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command transport backs the snippets on the doc-site // "Transport" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos (construction and wiring). // No outbound HTTP calls are issued. package main import ( "log" "net/http" "net/url" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" ) // --- Stubs (excluded from rendered snippets) ------------------------ // host, base, schemes stand in for the values a generated client would // pass to client.NewWithClient. They are referenced from snippets that // would otherwise need full literals on every line. var ( host = "api.example.com" base = "/v1" schemes = []string{"https"} //nolint:goconst // doc example: kept literal for snippet readability ) // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { registerVendorCodec() if _, err := setupMutualTLS(); err != nil { log.Printf("TLS setup (expected to fail without certs): %v", err) } timeoutClient() proxyFromEnv() proxyExplicit() enableConnectionReuse() } // --- Snippets ------------------------------------------------------- func registerVendorCodec() { // snippet:registerVendorCodec rt := client.New("api.example.com", "/v1", []string{"https"}) rt.Consumers["application/vnd.acme.v1+json"] = runtime.JSONConsumer() rt.Producers["application/vnd.acme.v1+json"] = runtime.JSONProducer() // endsnippet:registerVendorCodec use(rt) } func setupMutualTLS() (*client.Runtime, error) { // snippet:setupMutualTLS tlsCfg, err := client.TLSClientAuth(client.TLSClientOptions{ Certificate: "/etc/ssl/client.pem", Key: "/etc/ssl/client-key.pem", CA: "/etc/ssl/ca.pem", ServerName: "api.internal", }) if err != nil { return nil, err } httpClient := &http.Client{ Transport: &http.Transport{TLSClientConfig: tlsCfg}, } rt := client.NewWithClient("api.internal", "/v1", []string{"https"}, httpClient) // endsnippet:setupMutualTLS return rt, nil } func timeoutClient() { // snippet:timeoutClient httpClient := &http.Client{Timeout: client.DefaultTimeout} rt := client.NewWithClient(host, base, schemes, httpClient) // endsnippet:timeoutClient use(rt) } func proxyFromEnv() { // snippet:proxyFromEnv // Honour HTTPS_PROXY / HTTP_PROXY (default behaviour anyway). tr := &http.Transport{Proxy: http.ProxyFromEnvironment} httpClient := &http.Client{Transport: tr} rt := client.NewWithClient(host, base, schemes, httpClient) // endsnippet:proxyFromEnv use(rt) } func proxyExplicit() { // snippet:proxyExplicit // Force a specific proxy. proxyURL, _ := url.Parse("http://proxy.internal:3128") tr := &http.Transport{Proxy: http.ProxyURL(proxyURL)} httpClient := &http.Client{Transport: tr} rt := client.NewWithClient(host, base, schemes, httpClient) // endsnippet:proxyExplicit use(rt) } func enableConnectionReuse() { // snippet:enableConnectionReuse rt := client.New("api.example.com", "/v1", []string{"https"}) rt.EnableConnectionReuse() // endsnippet:enableConnectionReuse use(rt) } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/����������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023362�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/contenttyper/���������������������������������0000775�0000000�0000000�00000000000�15202323100�0026120�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/contenttyper/main.go��������������������������0000664�0000000�0000000�00000006366�15202323100�0027406�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command contenttyper backs the snippets on the doc-site // "Per-payload Content-Type override" page. Each function below is the // source of a `{{< code region="..." >}}` include; the package as a // whole compiles and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos (no network is touched — // the fake transport just records the picked Content-Type). package main import ( "io" "os" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" ) // --- Stubs (excluded from rendered snippets) ------------------------ // fakeTransport stands in for an application's runtime.ClientTransport // (typically a client.Runtime). Submit is a no-op so the demos can run // without a real server. type fakeTransport struct{} func (fakeTransport) Submit(_ *runtime.ClientOperation) (any, error) { return struct{}{}, nil } // putAvatarBody stands in for a generated request-writer builder. In // generated client code this would be produced by go-swagger from the // operation's body parameter and would attach the payload via // ClientRequest.SetBodyParam. func putAvatarBody(body any) runtime.ClientRequestWriter { return runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetBodyParam(body) }) } // putAvatarReader stands in for a generated response reader. type putAvatarReader struct{} func (putAvatarReader) ReadResponse(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return struct{}{}, nil } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { streamPayloadDemo() multipartFilePartDemo() } func streamPayloadDemo() { // Use a throwaway file so the demo runs without an external avatar. tmp, err := os.CreateTemp("", "avatar-*.png") if err != nil { return } tmp.Close() defer os.Remove(tmp.Name()) _ = uploadAvatar(fakeTransport{}, tmp.Name()) } func multipartFilePartDemo() { tmp, err := os.CreateTemp("", "manifest-*.json") if err != nil { return } tmp.Close() defer os.Remove(tmp.Name()) f, _ := os.Open(tmp.Name()) defer f.Close() part := taggedFile{File: f, mime: "application/vnd.acme.manifest+json"} use(part) } // --- Snippets ------------------------------------------------------- // snippet:streamPayload type imagePayload struct { body io.Reader mime string } func (p imagePayload) Read(b []byte) (int, error) { return p.body.Read(b) } // ContentTyper — wins over the operation's `consumes` default. func (p imagePayload) ContentType() string { return p.mime } func uploadAvatar(rt runtime.ClientTransport, avatar string) error { f, _ := os.Open(avatar) defer f.Close() op := &runtime.ClientOperation{ ID: "UploadAvatar", Method: "PUT", PathPattern: "/users/me/avatar", Params: putAvatarBody(imagePayload{ body: f, mime: "image/png", // ← will land on the wire as Content-Type }), Reader: putAvatarReader{}, } _, err := rt.Submit(op) return err } // endsnippet:streamPayload // snippet:multipartFileType type taggedFile struct { *os.File mime string } func (t taggedFile) ContentType() string { return t.mime } // endsnippet:multipartFileType ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/customcodec/����������������������������������0000775�0000000�0000000�00000000000�15202323100�0025672�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/customcodec/main.go���������������������������0000664�0000000�0000000�00000007542�15202323100�0027155�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command customcodec backs the snippets on the doc-site // "Custom codec (MessagePack)" recipe page. Each region below is the // source of a `{{< code region="..." >}}` include; the package as a // whole compiles and lints so the snippets cannot rot silently. // // The example uses MessagePack as the worked content-type, but the // real `github.com/vmihailenco/msgpack/v5` dependency is replaced by // a tiny in-file shim so this recipe doesn't add a third-party module // to the examples go.mod. The codec contract (Consumer / Producer) is // what the recipe is about — the wire format is incidental. // // `go run .` exercises the non-blocking demos (registration wiring). package main import ( "io" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" "github.com/go-openapi/runtime/middleware/untyped" ) // --- Stubs (excluded from rendered snippets) ------------------------ // doc is a placeholder OpenAPI document. The snippets pretend it was // loaded from disk; the demo wires nil so the program compiles and // runs without an on-disk spec. var doc *loads.Document // op stands in for a generated client operation whose media-type lists // the caller may want to override per-call. var op = &runtime.ClientOperation{} // msgpack mimics the surface of github.com/vmihailenco/msgpack/v5 used // by the recipe (NewDecoder / NewEncoder). It exists only so the // snippet bodies compile without pulling the real dependency into the // examples module. Real applications import vmihailenco/msgpack/v5. var msgpack = msgpackShim{} type msgpackShim struct{} func (msgpackShim) NewDecoder(r io.Reader) *msgpackDecoder { return &msgpackDecoder{r: r} } func (msgpackShim) NewEncoder(w io.Writer) *msgpackEncoder { return &msgpackEncoder{w: w} } type msgpackDecoder struct{ r io.Reader } func (d *msgpackDecoder) Decode(_ any) error { // Drain the reader so the stub behaves like a real decoder w.r.t. io. _, err := io.Copy(io.Discard, d.r) return err } type msgpackEncoder struct{ w io.Writer } func (e *msgpackEncoder) Encode(_ any) error { _, err := e.w.Write(nil) return err } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { registerOnServer() registerOnClient() operationMediaTypes() } // --- Snippets ------------------------------------------------------- // snippet:consumerProducerPair // Mime is the content-type the recipe registers under. MessagePack has // no IANA-registered MIME; application/x-msgpack and application/msgpack // are both common — pick one and stick to it. const Mime = "application/x-msgpack" // Consumer returns a runtime.Consumer that decodes a MessagePack body // into the target value v. func Consumer() runtime.Consumer { return runtime.ConsumerFunc(func(r io.Reader, v any) error { return msgpack.NewDecoder(r).Decode(v) }) } // Producer returns a runtime.Producer that serialises v as MessagePack // onto w. func Producer() runtime.Producer { return runtime.ProducerFunc(func(w io.Writer, v any) error { return msgpack.NewEncoder(w).Encode(v) }) } // endsnippet:consumerProducerPair func registerOnServer() { // snippet:registerOnServer api := untyped.NewAPI(doc).WithJSONDefaults() // JSON codecs registered for free api.RegisterConsumer(Mime, Consumer()) api.RegisterProducer(Mime, Producer()) // endsnippet:registerOnServer use(api) } func registerOnClient() { // snippet:registerOnClient rt := client.New("api.example.com", "/v1", []string{"https"}) rt.Consumers[Mime] = Consumer() rt.Producers[Mime] = Producer() // endsnippet:registerOnClient use(rt) } func operationMediaTypes() { // snippet:operationMediaTypes op.ConsumesMediaTypes = []string{Mime} op.ProducesMediaTypes = []string{Mime} // endsnippet:operationMediaTypes } ��������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/negotiatestandalone/��������������������������0000775�0000000�0000000�00000000000�15202323100�0027412�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/negotiatestandalone/main.go�������������������0000664�0000000�0000000�00000004171�15202323100�0030670�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command negotiatestandalone backs the snippets on the doc-site // "Negotiation in plain net/http" page. Each function below is the source // of a `{{< code region="..." >}}` include; the package as a whole // compiles and lints so the snippets cannot rot silently. // // `go run .` exits immediately (no demos run by default). Pass `serve` to // run the negotiating HTTP server demo (which blocks on ListenAndServe). package main import ( "encoding/json" "encoding/xml" "log" "net/http" "os" "time" "github.com/go-openapi/runtime/server-middleware/negotiate" ) // --- Stubs (excluded from rendered snippets) ------------------------ // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} const readHeaderTimeout = 5 * time.Second func main() { ignoreParameters(nil) if len(os.Args) > 1 && os.Args[1] == "serve" { pickContentType() } } // --- Snippets ------------------------------------------------------- // snippet:pickContentType const mediaTypeXML = "application/xml" // Pet is the demo resource served by the negotiation handler. type Pet struct { XMLName xml.Name `json:"-" xml:"pet"` Name string `json:"name" xml:"name"` } func pickContentType() { pet := Pet{Name: "Lassie"} offers := []string{"application/json", mediaTypeXML} http.HandleFunc("/pet", func(w http.ResponseWriter, r *http.Request) { chosen := negotiate.ContentType(r, offers, "application/json") w.Header().Set("Content-Type", chosen) switch chosen { case mediaTypeXML: _ = xml.NewEncoder(w).Encode(pet) default: _ = json.NewEncoder(w).Encode(pet) } }) srv := &http.Server{ Addr: ":8080", ReadHeaderTimeout: readHeaderTimeout, } log.Fatal(srv.ListenAndServe()) } // endsnippet:pickContentType func ignoreParameters(r *http.Request) { if r == nil { return } offers := []string{"application/json", mediaTypeXML} // snippet:ignoreParameters chosen := negotiate.ContentType(r, offers, "", negotiate.WithIgnoreParameters(true), ) // endsnippet:ignoreParameters use(chosen) } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/streamingbodies/������������������������������0000775�0000000�0000000�00000000000�15202323100�0026541�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/streamingbodies/main.go�����������������������0000664�0000000�0000000�00000012422�15202323100�0030015�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command streamingbodies backs the snippets on the doc-site // "Streaming bodies" example page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // `go run .` is a no-op — every demo here either configures stateful // API objects or expects a live HTTP server. Pass `serve` to actually // start the untyped HTTP server demo (which blocks on ListenAndServe). package main import ( "context" "errors" "io" "log" "net/http" "os" "strings" "time" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/strfmt" ) // --- Stubs (excluded from rendered snippets) ------------------------ // api and rt are package-level stand-ins so the snippet bodies read // like in-context code. They stay nil — every demo function bails // early when its prerequisites are missing, so `go run .` stays cheap. var ( api *untyped.API rt *client.Runtime ctx = context.Background() ) // putBackupParams is the bound parameter struct the untyped runtime // would synthesize from the spec. Only the `Blob` field is exercised // by the upload snippet. type putBackupParams struct { Blob io.ReadCloser } // putBackupRequest writes the streaming body for the client snippet. type putBackupRequest struct { body io.Reader } func (p putBackupRequest) WriteToRequest(req runtime.ClientRequest, _ strfmt.Registry) error { return req.SetBodyParam(p.body) } // putBackupResponse reads the (empty) response body for the client snippet. type putBackupResponse struct{} func (putBackupResponse) ReadResponse(_ runtime.ClientResponse, _ runtime.Consumer) (any, error) { return struct{}{}, nil } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} const readHeaderTimeout = 5 * time.Second func main() { if len(os.Args) > 1 && os.Args[1] == "serve" { serverDownload() } consumerWithCloses() serverUpload() clientStream() } // --- Snippets ------------------------------------------------------- func serverDownload() { doc, _ := loads.Spec("api.yaml") // snippet:serverDownload api := untyped.NewAPI(doc).WithJSONDefaults() // ByteStreamProducer is registered by WithJSONDefaults under // runtime.DefaultMime ("application/octet-stream"), but be explicit // when more than one stream-producing MIME is in the picture: api.RegisterProducer(runtime.DefaultMime, runtime.ByteStreamProducer()) api.RegisterOperation("get", "/backups/{id}", runtime.OperationHandlerFunc( func(_ any) (any, error) { f, err := os.Open("/var/backups/2026-05-10.tar") if err != nil { return nil, err } // The Producer copies whatever io.Reader you return into the // response writer. Returning *os.File is fine; close it from // a Responder if you need ownership semantics. return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) { defer f.Close() w.Header().Set("Content-Type", runtime.DefaultMime) _ = p.Produce(w, f) }), nil }, )) // endsnippet:serverDownload srv := &http.Server{ Addr: ":8080", Handler: middleware.Serve(doc, api), ReadHeaderTimeout: readHeaderTimeout, } log.Fatal(srv.ListenAndServe()) } func consumerWithCloses() { if api == nil { return } // snippet:consumerWithCloses api.RegisterConsumer(runtime.DefaultMime, runtime.ByteStreamConsumer( runtime.ClosesStream, // closes the io.ReadCloser when done )) // endsnippet:consumerWithCloses } func serverUpload() { if api == nil { return } // snippet:serverUpload api.RegisterOperation("post", "/backups", runtime.OperationHandlerFunc( func(params any) (any, error) { putBackupParams, ok := params.(putBackupParams) if !ok { return nil, errors.New("invalid params") } body := putBackupParams.Blob // io.ReadCloser defer body.Close() f, err := os.CreateTemp("", "upload-*") if err != nil { return nil, err } defer f.Close() if _, err := io.Copy(f, body); err != nil { return nil, err } return map[string]string{"status": "ok"}, nil }, )) // endsnippet:serverUpload } func clientStream() { if rt == nil { // keep the demo non-blocking: provide a fake reader instead of // touching the filesystem, and bail before the real Submit call. body := strings.NewReader("fake backup contents") op := &runtime.ClientOperation{ ID: "PutBackup", Method: "POST", PathPattern: "/backups", ConsumesMediaTypes: []string{runtime.DefaultMime}, Params: putBackupRequest{body: body}, Reader: putBackupResponse{}, } use(op) return } // snippet:clientStream file, _ := os.Open("./backup.tar") defer file.Close() op := &runtime.ClientOperation{ ID: "PutBackup", Method: "POST", PathPattern: "/backups", ConsumesMediaTypes: []string{runtime.DefaultMime}, Params: putBackupRequest{body: file}, Reader: putBackupResponse{}, } _, err := rt.SubmitContext(ctx, op) // endsnippet:clientStream use(err) } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/vendortypes/����������������������������������0000775�0000000�0000000�00000000000�15202323100�0025744�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/contenttypes/vendortypes/main.go���������������������������0000664�0000000�0000000�00000004302�15202323100�0027216�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command vendortypes backs the snippets on the doc-site // "Vendor MIME types" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos (no network is touched — // the demos only register codecs and dispatch on a fake request). package main import ( "net/http" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware/untyped" ) // --- Stubs (excluded from rendered snippets) ------------------------ var doc *loads.Document // handleV1 / handleV2 stand in for the per-version handler bodies the // caller would supply. They return an empty payload so the snippet // type-checks without dragging in a domain model. func handleV1(_ any) (any, error) { return struct{}{}, nil } func handleV2(_ any) (any, error) { return struct{}{}, nil } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { registerVendorTypes() dispatchOnContentType() } // --- Snippets ------------------------------------------------------- func registerVendorTypes() { // snippet:registerVendorTypes api := untyped.NewAPI(doc).WithJSONDefaults() api.RegisterConsumer("application/vnd.acme.v1+json", runtime.JSONConsumer()) api.RegisterProducer("application/vnd.acme.v1+json", runtime.JSONProducer()) api.RegisterConsumer("application/vnd.acme.v2+json", runtime.JSONConsumer()) api.RegisterProducer("application/vnd.acme.v2+json", runtime.JSONProducer()) // endsnippet:registerVendorTypes use(api) } func dispatchOnContentType() { // snippet:dispatchOnContentType handlePost := func(r *http.Request, body any) (any, error) { ct, _, _ := runtime.ContentType(r.Header) switch ct { case "application/vnd.acme.v1+json": return handleV1(body) case "application/vnd.acme.v2+json": return handleV2(body) } return nil, errors.New(http.StatusUnsupportedMediaType, "unsupported version") } // endsnippet:dispatchOnContentType use(handlePost) } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/core/������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0021553�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/core/contenttypes/�����������������������������������������0000775�0000000�0000000�00000000000�15202323100�0024312�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/core/contenttypes/main.go����������������������������������0000664�0000000�0000000�00000003372�15202323100�0025572�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command contenttypes backs the snippets on the doc-site // "Content types" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos (codec registration). package main import ( "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/client" "github.com/go-openapi/runtime/middleware/untyped" ) // --- Stubs (excluded from rendered snippets) ------------------------ // spec is a placeholder OpenAPI document. Snippets pretend it was // loaded from disk; the demo wires a nil spec so the program compiles // and runs. var spec *loads.Document // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { registerCodecsServer() registerCodecsClient() } // --- Snippets ------------------------------------------------------- func registerCodecsServer() { // snippet:registerCodecsServer api := untyped.NewAPI(spec) api.RegisterConsumer(runtime.JSONMime, runtime.JSONConsumer()) api.RegisterProducer(runtime.JSONMime, runtime.JSONProducer()) api.RegisterConsumer("application/vnd.acme.v1+json", runtime.JSONConsumer()) api.RegisterProducer("application/vnd.acme.v1+json", runtime.JSONProducer()) // endsnippet:registerCodecsServer use(api) } func registerCodecsClient() { // snippet:registerCodecsClient rt := client.New("api.example.com", "/v1", []string{"https"}) rt.Consumers[runtime.JSONMime] = runtime.JSONConsumer() rt.Producers[runtime.JSONMime] = runtime.JSONProducer() // endsnippet:registerCodecsClient use(rt) } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/core/validation/�������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023705�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/core/validation/main.go������������������������������������0000664�0000000�0000000�00000005432�15202323100�0025164�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command validation backs the snippets on the doc-site // "Validation hooks" page. Each type below is the source of a // `{{< code region="..." >}}` include; the package as a whole // compiles and lints so the snippets cannot rot silently. // // `go run .` exercises the validators against canned inputs. package main import ( "context" "errors" "log" "time" "github.com/go-openapi/strfmt" ) // --- Stubs (excluded from rendered snippets) ------------------------ // userKey is the context-key type used by the fake reqUser helper. type userKey struct{} // reqUser pretends to extract the authenticated user from a request // context. Real code would read from a request-scoped middleware value. func reqUser(ctx context.Context) string { if v, ok := ctx.Value(userKey{}).(string); ok { return v } return "" } func main() { dateRangeValidation() contextValidation() } func dateRangeValidation() { from := strfmt.Date(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)) to := strfmt.Date(time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)) good := DateRange{From: from, To: to} bad := DateRange{From: to, To: from} log.Println("good DateRange:", good.Validate(strfmt.Default)) log.Println("bad DateRange:", bad.Validate(strfmt.Default)) } func contextValidation() { anon := context.Background() authed := context.WithValue(context.Background(), userKey{}, "alice") req := MyRequest{OnBehalfOf: "bob"} log.Println("anonymous on_behalf_of:", req.ContextValidate(anon, strfmt.Default)) log.Println("authenticated on_behalf_of:", req.ContextValidate(authed, strfmt.Default)) } // --- Snippets ------------------------------------------------------- // snippet:dateRangeValidate // DateRange illustrates a cross-field invariant on a hand-written type. type DateRange struct { From strfmt.Date `json:"from"` To strfmt.Date `json:"to"` } // Validate enforces that To is not before From. The strfmt.Registry // argument is unused here because the rule does not involve any // registered string format. func (d DateRange) Validate(_ strfmt.Registry) error { if time.Time(d.To).Before(time.Time(d.From)) { return errors.New("DateRange.to must not be before DateRange.from") } return nil } // endsnippet:dateRangeValidate // MyRequest is a hand-written request type that demonstrates a // context-aware validation rule. type MyRequest struct { OnBehalfOf string `json:"onBehalfOf"` } // snippet:contextValidate // ContextValidate enforces that on_behalf_of is only set when the // request context carries an authenticated user. func (r MyRequest) ContextValidate(ctx context.Context, _ strfmt.Registry) error { if reqUser(ctx) == "" && r.OnBehalfOf != "" { return errors.New("on_behalf_of is only valid when authenticated") } return nil } // endsnippet:contextValidate ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/customcodec/�����������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023133�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/customcodec/uint32.go��������������������������������������0000664�0000000�0000000�00000001362�15202323100�0024610�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Package customcodec illustrates how to implement a runtime.Consumer for a // custom wire format. Uint32Consumer decodes a single big-endian 32-bit // unsigned integer from the request body into a *uint32 target. package customcodec import ( "encoding/binary" "fmt" "io" "github.com/go-openapi/runtime" ) // Uint32Consumer returns a runtime.Consumer that reads a single big-endian // uint32 from r and stores it at v (which must be a *uint32). func Uint32Consumer() runtime.Consumer { return runtime.ConsumerFunc(func(r io.Reader, v any) error { p, ok := v.(*uint32) if !ok { return fmt.Errorf("uint32 consumer: target %T is not *uint32", v) } return binary.Read(r, binary.BigEndian, p) }) } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/customcodec/uint32_test.go���������������������������������0000664�0000000�0000000�00000001260�15202323100�0025644�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 package customcodec import ( "bytes" "encoding/binary" "strings" "testing" "github.com/go-openapi/testify/v2/require" ) func TestUint32Consumer_DecodesBigEndian(t *testing.T) { var buf bytes.Buffer require.NoError(t, binary.Write(&buf, binary.BigEndian, uint32(0xCAFEBABE))) var got uint32 require.NoError(t, Uint32Consumer().Consume(&buf, &got)) require.Equal(t, uint32(0xCAFEBABE), got) } func TestUint32Consumer_RejectsWrongTargetType(t *testing.T) { var notAUint32 int err := Uint32Consumer().Consume(strings.NewReader("\x00\x00\x00\x01"), ¬AUint32) require.Error(t, err) require.Contains(t, err.Error(), "not *uint32") } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/doc.go�����������������������������������������������������0000664�0000000�0000000�00000000404�15202323100�0021715�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// Package examples contains many code examples. // // They illustrate how to use [github.com/go-openapi/runtime]. // // Code snippets are injected in the [documentation site]. // // [documentation site]: https://go-openapi.github.io/runtime> package examples ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/go.mod�����������������������������������������������������0000664�0000000�0000000�00000004333�15202323100�0021734�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// Module github.com/go-openapi/runtime/docs/examples hosts runnable code // samples referenced from the documentation site. It is intentionally kept // separate from the root module so example dependencies do not leak into // runtime consumers. module github.com/go-openapi/runtime/docs/examples go 1.25.0 require ( github.com/CAFxX/httpcompression v0.0.9 github.com/go-openapi/analysis v0.25.0 github.com/go-openapi/errors v0.22.7 github.com/go-openapi/loads v0.23.3 github.com/go-openapi/runtime v0.0.0 github.com/go-openapi/runtime/server-middleware v0.30.0 github.com/go-openapi/strfmt v0.26.2 github.com/go-openapi/testify/v2 v2.5.0 github.com/justinas/alice v1.2.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/spec v0.22.4 // indirect github.com/go-openapi/swag/conv v0.26.0 // indirect github.com/go-openapi/swag/fileutils v0.26.0 // indirect github.com/go-openapi/swag/jsonname v0.26.0 // indirect github.com/go-openapi/swag/jsonutils v0.26.0 // indirect github.com/go-openapi/swag/loading v0.26.0 // indirect github.com/go-openapi/swag/mangling v0.26.0 // indirect github.com/go-openapi/swag/stringutils v0.26.0 // indirect github.com/go-openapi/swag/typeutils v0.26.0 // indirect github.com/go-openapi/swag/yamlutils v0.26.0 // indirect github.com/go-openapi/validate v0.25.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.6 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.54.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.37.0 // indirect ) replace ( github.com/go-openapi/runtime => ../.. github.com/go-openapi/runtime/server-middleware => ../../server-middleware ) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/go.sum�����������������������������������������������������0000664�0000000�0000000�00000025175�15202323100�0021770�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg= github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs= github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/strfmt v0.26.2 h1:ysjheCh4i1rmFEo2LanhELDNucNzfWTZhUDKgWWPaFM= github.com/go-openapi/strfmt v0.26.2/go.mod h1:fXh1e449cyUn2NYuz+wb3wARBUdMl7qPEZwX00nqivY= github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= github.com/go-openapi/testify/enable/yaml/v2 v2.5.0 h1:3hZD1fwydvCx/cc1R2uYNQirHqf2s6lqpKV3FcNTURA= github.com/go-openapi/testify/enable/yaml/v2 v2.5.0/go.mod h1:TvDZKBH7ZbMaF3EqH2AwTvNQCmzyZq8K1agRjf1B+Nk= github.com/go-openapi/testify/v2 v2.5.0 h1:UOCr63aAsMIDydZbZGqo5Ev01D4eydItRbekDuZMJLw= github.com/go-openapi/testify/v2 v2.5.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k= github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g= github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/middleware/������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0022740�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/middleware/compression/������������������������������������0000775�0000000�0000000�00000000000�15202323100�0025301�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/middleware/compression/main.go�����������������������������0000664�0000000�0000000�00000007651�15202323100�0026565�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // Package main shows how to add transparent response compression // (gzip, brotli, etc.) to a go-openapi server by wrapping the // http.Handler returned by middleware.Serve with the CAFxX // httpcompression adapter. // // The runtime does not ship compression itself; this example is a // recipe for composing the existing ecosystem with the go-openapi // pipeline. // // Run: // // go run . // curl -sH 'Accept-Encoding: gzip' -i http://localhost:8080/api/greeting | head package main import ( "encoding/json" "log" "net/http" "time" httpcompression "github.com/CAFxX/httpcompression" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware/untyped" ) const ( // greetingPayloadKeys keeps the response body comfortably above // the compression middleware's MinSize threshold so the example // actually exercises the compressor instead of falling through. greetingPayloadKeys = 32 listenAddress = ":8080" readHeaderTimeout = 5 * time.Second ) const swaggerSpec = `{ "swagger": "2.0", "info": {"title": "Compression Demo", "version": "1.0"}, "basePath": "/api", "consumes": ["application/json"], "produces": ["application/json"], "paths": { "/greeting": { "get": { "operationId": "greeting", "responses": { "200": { "description": "a greeting payload large enough to be worth compressing", "schema": {"type": "object"} } } } } } }` // greeting returns a payload large enough to clear the compression // middleware's default minimum-size threshold (CAFxX defaults to // DefaultMinSize = 200 bytes — payloads below that are left // uncompressed because the wire overhead would outweigh the win). var greeting = runtime.OperationHandlerFunc(func(_ any) (any, error) { greetings := make(map[string]string, greetingPayloadKeys) for i := range greetingPayloadKeys { greetings[encodeKey(i)] = "Hello from go-openapi! Compression makes this body smaller on the wire." } return greetings, nil }) func encodeKey(i int) string { return "greeting_" + string(rune('a'+i%26)) } func newAPI() (http.Handler, error) { spec, err := loads.Analyzed(json.RawMessage(swaggerSpec), "") if err != nil { return nil, err } api := untyped.NewAPI(spec) api.RegisterOperation("get", "/greeting", greeting) return middleware.Serve(spec, api), nil } func main() { apiHandler, err := newAPI() if err != nil { log.Fatalf("build api: %v", err) } // DefaultAdapter wires gzip + brotli encoders with sane defaults: // content-type allowlist, minimum-size threshold, Vary and // Content-Length handling, ETag suffixing for cacheability. // // Use Adapter (without "Default") for explicit codec + threshold // control: // // compress, err := httpcompression.Adapter( // httpcompression.GzipCompressionLevel(6), // httpcompression.BrotliCompressionLevel(4), // httpcompression.MinSize(512), // httpcompression.ContentTypes([]string{"application/json"}, false), // ) // snippet:compressionWiring compress, err := httpcompression.DefaultAdapter() if err != nil { log.Fatalf("compression adapter: %v", err) } // Wrap the go-openapi handler. The order matters: // - the compressor must be OUTSIDE the api pipeline so it sees // the final response bytes; // - any TLS / auth / rate-limiting middleware typically wraps // the compressor (i.e. compressor sits between application // code and transport-level middleware). mux := http.NewServeMux() mux.Handle("/", compress(apiHandler)) // endsnippet:compressionWiring log.Printf("listening on %s", listenAddress) srv := &http.Server{ Addr: listenAddress, Handler: mux, ReadHeaderTimeout: readHeaderTimeout, } log.Fatal(srv.ListenAndServe()) } ���������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/server/����������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0022131�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/server/binding/��������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023543�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/server/binding/main.go�������������������������������������0000664�0000000�0000000�00000003670�15202323100�0025024�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command binding backs the snippets on the doc-site // "Parameter binding & validation" page. Each function below is the // source of a `{{< code region="..." >}}` include; the package as a // whole compiles and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos. package main import ( "net/http" "github.com/go-openapi/loads" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware/untyped" ) // --- Stubs (excluded from rendered snippets) ------------------------ // spec is a placeholder OpenAPI document. Snippets pretend it was // loaded from disk; the demo leaves it nil so the program compiles. var ( spec *loads.Document api *untyped.API ) // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} // extraMiddleware is a placeholder Builder demonstrating how an extra // middleware layer can read the matched route from the request // context without rebinding. func extraMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { readMatchedRoute(r) next.ServeHTTP(w, r) }) } func main() { ignoreParameters() use(extraMiddleware) } // --- Snippets ------------------------------------------------------- func ignoreParameters() { // snippet:ignoreParameters ctx := middleware.NewContext(spec, api, nil).SetIgnoreParameters(true) handler := ctx.APIHandler(middleware.PassthroughBuilder) // endsnippet:ignoreParameters use(handler) } func readMatchedRoute(r *http.Request) { // snippet:readMatchedRoute // inside middleware.Builder match := middleware.MatchedRouteFrom(r) // (no public accessor for the bound struct itself today — // re-call BindValidRequest if you need it; the result is cached // so a second call is cheap) // endsnippet:readMatchedRoute use(match) } ������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/server/deprecatedshims/������������������������������������0000775�0000000�0000000�00000000000�15202323100�0025275�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/server/deprecatedshims/main.go�����������������������������0000664�0000000�0000000�00000005757�15202323100�0026566�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command deprecatedshims backs the snippets on the doc-site // "Deprecated shims" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // The page shows the old (deprecated) middleware entry points side-by- // side with their new server-middleware equivalents. Both halves are // exercised here so neither path can rot. // // `go run .` exercises the non-blocking demos. package main import ( "context" "net/http" "net/http/httptest" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/server-middleware/docui" "github.com/go-openapi/runtime/server-middleware/negotiate" ) // --- Stubs (excluded from rendered snippets) ------------------------ // api is a placeholder downstream handler that the doc-UI middleware // wraps. Snippets pretend it is the application's API mux. var api = http.NotFoundHandler() // newRequest builds a throwaway *http.Request with an Accept header so // the negotiation snippets have something to chew on. func newRequest() *http.Request { r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) r.Header.Set("Accept", "application/json") return r } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { negotiateBefore() negotiateAfter() swaggerUIBefore() swaggerUIAfter() } // --- Snippets ------------------------------------------------------- func negotiateBefore() { r := newRequest() offers := []string{"application/json", "application/xml"} // snippet:negotiateBefore // before // import "github.com/go-openapi/runtime/middleware" chosen := middleware.NegotiateContentType(r, offers, "application/json") //nolint:staticcheck // intentionally demonstrating deprecated API // endsnippet:negotiateBefore use(chosen) } func negotiateAfter() { r := newRequest() offers := []string{"application/json", "application/xml"} // snippet:negotiateAfter // after // import "github.com/go-openapi/runtime/server-middleware/negotiate" chosen := negotiate.ContentType(r, offers, "application/json") // endsnippet:negotiateAfter use(chosen) } func swaggerUIBefore() { // snippet:swaggerUIBefore // before // import "github.com/go-openapi/runtime/middleware" handler := middleware.SwaggerUI(middleware.SwaggerUIOpts{ //nolint:staticcheck // intentionally demonstrating deprecated API BasePath: "/", Path: "docs", SpecURL: "/swagger.json", Title: "Pet store", }, api) // endsnippet:swaggerUIBefore use(handler) } func swaggerUIAfter() { // snippet:swaggerUIAfter // after // import "github.com/go-openapi/runtime/server-middleware/docui" handler := docui.SwaggerUI(api, docui.WithUIBasePath("/"), docui.WithUIPath("docs"), docui.WithSpecURL("/swagger.json"), docui.WithUITitle("Pet store"), ) // endsnippet:swaggerUIAfter use(handler) } �����������������go-openapi-runtime-decad8f/docs/examples/server/pipeline/�������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023736�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/server/pipeline/main.go������������������������������������0000664�0000000�0000000�00000005441�15202323100�0025215�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command pipeline backs the snippets on the doc-site // "Request pipeline" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole // compiles and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos. Pass `serve` to run the // untyped HTTP server demo (which blocks on ListenAndServe). package main import ( "log" "net/http" "os" "time" "github.com/justinas/alice" "github.com/go-openapi/analysis" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware/untyped" ) // --- Stubs (excluded from rendered snippets) ------------------------ var ( spec *loads.Document api *untyped.API myAPI middleware.RoutableAPI analyzed *analysis.Spec doc *loads.Document ) var myGetPetHandler = runtime.OperationHandlerFunc(func(_ any) (any, error) { return struct{}{}, nil }) func loggingMW(next http.Handler) http.Handler { return next } func rateLimitMW(next http.Handler) http.Handler { return next } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { contextConstructors() aliceComposition() if len(os.Args) > 1 && os.Args[1] == "serve" { untypedServer() } } // --- Snippets ------------------------------------------------------- func contextConstructors() { // snippet:contextConstructors // Default — untyped.API wrapped in a routableUntypedAPI. ctxDefault := middleware.NewContext(spec, api, nil) // Custom — anything that implements RoutableAPI. ctxCustom := middleware.NewRoutableContext(spec, myAPI, nil) // Same, with a pre-analyzed spec to skip re-analysis. ctxAnalyzed := middleware.NewRoutableContextWithAnalyzedSpec(spec, analyzed, myAPI, nil) // endsnippet:contextConstructors use(ctxDefault, ctxCustom, ctxAnalyzed) } const readHeaderTimeout = 5 * time.Second func untypedServer() { // snippet:untypedServer doc, _ := loads.Spec("api.yaml") api := untyped.NewAPI(doc) api.RegisterConsumer(runtime.JSONMime, runtime.JSONConsumer()) api.RegisterProducer(runtime.JSONMime, runtime.JSONProducer()) api.RegisterOperation("get", "/pets/{id}", myGetPetHandler) srv := &http.Server{ Addr: ":8080", Handler: middleware.Serve(doc, api), ReadHeaderTimeout: readHeaderTimeout, } log.Fatal(srv.ListenAndServe()) // endsnippet:untypedServer } func aliceComposition() { // snippet:aliceComposition decorate := func(next http.Handler) http.Handler { return alice.New(loggingMW, rateLimitMW).Then(next) } handler := middleware.ServeWithBuilder(doc, api, decorate) // endsnippet:aliceComposition use(handler) } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/server/security/�������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0024000�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/server/security/main.go������������������������������������0000664�0000000�0000000�00000011740�15202323100�0025256�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command security backs the snippets on the doc-site // "Security schemes" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // `go run .` exercises the non-blocking demos (wiring registration). package main import ( "context" "net/http" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/runtime/security" ) // --- Stubs (excluded from rendered snippets) ------------------------ // doc is a placeholder OpenAPI document. Snippets pretend it was loaded // from disk; the demo wires a freshly-constructed empty spec so the // program compiles and runs. var doc *loads.Document // appPrincipal stands in for whatever the application returns from // authentication (e.g. *models.Principal). type appPrincipal struct { ID string Email string scopes []string } // HasScopes reports whether the principal carries every required scope. func (p *appPrincipal) HasScopes(required []string) bool { have := make(map[string]struct{}, len(p.scopes)) for _, s := range p.scopes { have[s] = struct{}{} } for _, s := range required { if _, ok := have[s]; !ok { return false } } return true } // fakeStore stands in for an application-supplied user / token store. type fakeStore struct{} func (fakeStore) AuthenticateBasic(_ context.Context, user, _ string) (*appPrincipal, error) { if user == "" { return nil, errors.Unauthenticated("basic") } return &appPrincipal{ID: user, Email: user + "@example.com"}, nil } func (fakeStore) AuthenticateAPIKey(token string) (*appPrincipal, error) { if token == "" { return nil, errors.Unauthenticated("api-key") } return &appPrincipal{ID: token}, nil } var store = fakeStore{} // fakeTokens stands in for a JWT / opaque-token verifier. type fakeTokens struct{} func (fakeTokens) Verify(token string) (*appPrincipal, bool) { if token == "" { return nil, false } return &appPrincipal{ID: token, scopes: []string{"read:pets"}}, true } var tokens = fakeTokens{} // auditPkg stands in for an application-supplied audit / tracing // context helper — the doc snippet calls `audit.WithUser(ctx, id)`. type auditPkg struct{} type auditKey struct{} func (auditPkg) WithUser(ctx context.Context, id string) context.Context { return context.WithValue(ctx, auditKey{}, id) } var audit = auditPkg{} // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { basicAuthCtx() basicAuthSimple() apiKeyAuthHeader() bearerAuthScopes() registerAuthorized() readPrincipal(nil) } // --- Snippets ------------------------------------------------------- func basicAuthCtx() { // snippet:basicAuthCtx authn := security.BasicAuthCtx(func(ctx context.Context, user, pass string) (context.Context, any, error) { // request-scoped DB call honours ctx cancellation principal, err := store.AuthenticateBasic(ctx, user, pass) if err != nil { return ctx, nil, err } // enrich the context for downstream handlers ctx = audit.WithUser(ctx, principal.ID) return ctx, principal, nil }) // endsnippet:basicAuthCtx use(authn) } func basicAuthSimple() { // snippet:basicAuthSimple // principal type is up to you type Principal struct { ID string Email string } authn := security.BasicAuth(func(user, _ string) (any, error) { if user == "" { return nil, errors.Unauthenticated("basic") } return Principal{ID: user, Email: user + "@example.com"}, nil }) // endsnippet:basicAuthSimple use(authn) } func apiKeyAuthHeader() { // snippet:apiKeyAuthHeader authn := security.APIKeyAuth("X-Api-Key", "header", func(token string) (any, error) { return store.AuthenticateAPIKey(token) }, ) // endsnippet:apiKeyAuthHeader use(authn) } func bearerAuthScopes() { // snippet:bearerAuthScopes authn := security.BearerAuth("oauth2", func(token string, requiredScopes []string) (any, error) { principal, ok := tokens.Verify(token) if !ok { return nil, errors.Unauthenticated("bearer") } if !principal.HasScopes(requiredScopes) { return nil, errors.New(http.StatusForbidden, "insufficient_scope") } return principal, nil }, ) // endsnippet:bearerAuthScopes use(authn) } func registerAuthorized() { api := untyped.NewAPI(doc).WithJSONDefaults() // snippet:registerAuthorized api.RegisterAuthorizer(security.Authorized()) // always allow // endsnippet:registerAuthorized use(api) } func readPrincipal(r *http.Request) { if r == nil { // readPrincipal is invoked from main() for compile coverage; the // snippet body itself is what gets rendered into the docs. return } // snippet:readPrincipal principal := middleware.SecurityPrincipalFrom(r) scopes := middleware.SecurityScopesFrom(r) // endsnippet:readPrincipal use(principal, scopes) } ��������������������������������go-openapi-runtime-decad8f/docs/examples/standalone/������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0022753�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/standalone/contentnegotiation/�����������������������������0000775�0000000�0000000�00000000000�15202323100�0026666�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/standalone/contentnegotiation/main.go����������������������0000664�0000000�0000000�00000006075�15202323100�0030151�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command contentnegotiation backs the snippets on the doc-site // "Content negotiation" page (usage/standalone/content-negotiation.md). // Each function below is the source of a `{{< code region="..." >}}` // include; the package as a whole compiles and lints so the snippets // cannot rot silently. // // `go run .` exits immediately (no demos run by default). Pass `serve` to // run the negotiating HTTP server demo (which blocks on ListenAndServe). package main import ( "encoding/json" "encoding/xml" "log" "net/http" "os" "time" "github.com/go-openapi/loads" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/runtime/server-middleware/negotiate" "github.com/go-openapi/runtime/server-middleware/negotiate/header" ) // --- Stubs (excluded from rendered snippets) ------------------------ // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} const ( readHeaderTimeout = 5 * time.Second mediaTypeJSON = "application/json" mediaTypeXML = "application/xml" ) // Stubs for the server-wide opt-out snippet. They are package-level so // the snippet region itself stays focused on the single line that matters. var ( spec *loads.Document api *untyped.API ) func main() { ignoreParameters(nil) serverWideIgnoreParameters() parseAcceptHeader(nil) if len(os.Args) > 1 && os.Args[1] == "serve" { pickContentType() } } // --- Snippets ------------------------------------------------------- // snippet:pickContentType // Pet is the demo resource served by the negotiation handler. type Pet struct { XMLName xml.Name `json:"-" xml:"pet"` Name string `json:"name" xml:"name"` } func pickContentType() { pet := Pet{Name: "Lassie"} offers := []string{mediaTypeJSON, mediaTypeXML} http.HandleFunc("/pet", func(w http.ResponseWriter, r *http.Request) { chosen := negotiate.ContentType(r, offers, mediaTypeJSON) w.Header().Set("Content-Type", chosen) switch chosen { case mediaTypeXML: _ = xml.NewEncoder(w).Encode(pet) default: _ = json.NewEncoder(w).Encode(pet) } }) srv := &http.Server{ Addr: ":8080", ReadHeaderTimeout: readHeaderTimeout, } log.Fatal(srv.ListenAndServe()) } // endsnippet:pickContentType func ignoreParameters(r *http.Request) { if r == nil { return } offers := []string{mediaTypeJSON, mediaTypeXML} // snippet:ignoreParameters chosen := negotiate.ContentType(r, offers, "", negotiate.WithIgnoreParameters(true), ) // endsnippet:ignoreParameters use(chosen) } func serverWideIgnoreParameters() { // snippet:serverWideIgnoreParameters ctx := middleware.NewContext(spec, api, nil).SetIgnoreParameters(true) // endsnippet:serverWideIgnoreParameters use(ctx) } func parseAcceptHeader(r *http.Request) { if r == nil { return } // snippet:parseAcceptHeader specs := header.ParseAccept(r.Header, "Accept") for _, s := range specs { // s.Value, s.Q, s.Params use(s) } // endsnippet:parseAcceptHeader } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/standalone/docui/������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0024056�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/standalone/docui/main.go�����������������������������������0000664�0000000�0000000�00000007033�15202323100�0025334�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command docui backs the snippets on the doc-site // "Doc UIs & spec serving" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole compiles // and lints so the snippets cannot rot silently. // // `go run .` exits immediately (no demos run by default). Pass `serve` // to run the full Swagger UI + spec demo (which blocks on // ListenAndServe). package main import ( _ "embed" "log" "net/http" "os" "time" "github.com/go-openapi/runtime/server-middleware/docui" ) // --- Stubs (excluded from rendered snippets) ------------------------ // myAPIHandler returns the demo application handler used by the // snippets. It is intentionally minimal — the snippets only care that // something implementing http.Handler is available. func myAPIHandler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/v1/ping", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("pong")) }) return mux } // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} const readHeaderTimeout = 5 * time.Second //go:embed swagger.json var specBytes []byte func main() { directWrap(false) middlewareFactory() serveSpec() useSpec() pathFromOptions() if len(os.Args) > 1 && os.Args[1] == "serve" { puttingItTogether() } } // --- Snippets ------------------------------------------------------- func directWrap(listen bool) { // snippet:directWrap api := myAPIHandler() // your application handler := docui.SwaggerUI(api, docui.WithSpecURL("/swagger.json"), docui.WithUIBasePath("/"), docui.WithUIPath("docs"), ) srv := &http.Server{ Addr: ":8080", Handler: handler, ReadHeaderTimeout: readHeaderTimeout, } // endsnippet:directWrap if listen { log.Fatal(srv.ListenAndServe()) } use(srv) } func middlewareFactory() { api := myAPIHandler() mux := http.NewServeMux() // snippet:middlewareFactory mw := docui.UseSwaggerUI( docui.WithSpecURL("/swagger.json"), docui.WithUIPath("docs"), ) mux.Handle("/", mw(api)) // endsnippet:middlewareFactory use(mux) } func serveSpec() { api := myAPIHandler() // snippet:serveSpec handler := docui.ServeSpec(specBytes, api, docui.WithSpecPath("/swagger.json"), ) // endsnippet:serveSpec use(handler) } func useSpec() { api := myAPIHandler() mux := http.NewServeMux() // snippet:useSpec mw := docui.UseSpec(specBytes, docui.WithSpecPath("/swagger.json"), ) mux.Handle("/", mw(api)) // endsnippet:useSpec use(mux) } func pathFromOptions() { api := myAPIHandler() // snippet:pathFromOptions uiOpts := []docui.Option{docui.WithSpecURL("/swagger.json")} specOpt := docui.WithSpecPathFromOptions(uiOpts...) handler := docui.SwaggerUI( docui.ServeSpec(specBytes, api, specOpt), uiOpts..., ) // endsnippet:pathFromOptions use(handler) } // snippet:puttingItTogether //go:embed openapi.yaml var spec []byte func puttingItTogether() { api := http.NewServeMux() api.HandleFunc("/v1/ping", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("pong")) }) handler := docui.SwaggerUI( docui.ServeSpec(spec, api, docui.WithSpecPath("/openapi.yaml"), ), docui.WithSpecURL("/openapi.yaml"), docui.WithUIPath("docs"), docui.WithUITitle("Demo API"), ) srv := &http.Server{ Addr: ":8080", Handler: handler, ReadHeaderTimeout: readHeaderTimeout, } log.Fatal(srv.ListenAndServe()) } // endsnippet:puttingItTogether �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/standalone/docui/openapi.yaml������������������������������0000664�0000000�0000000�00000000220�15202323100�0026367�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������swagger: "2.0" info: title: Demo API version: 1.0.0 paths: /v1/ping: get: responses: "200": description: pong ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/standalone/docui/swagger.json������������������������������0000664�0000000�0000000�00000000373�15202323100�0026413�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "swagger": "2.0", "info": { "title": "Demo API", "version": "1.0.0" }, "paths": { "/v1/ping": { "get": { "responses": { "200": { "description": "pong" } } } } } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/standalone/mediatypes/�������������������������������������0000775�0000000�0000000�00000000000�15202323100�0025117�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/docs/examples/standalone/mediatypes/main.go������������������������������0000664�0000000�0000000�00000002212�15202323100�0026367�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: Apache-2.0 // Command mediatypes backs the snippets on the doc-site // "Media types" page. Each function below is the source of a // `{{< code region="..." >}}` include; the package as a whole // compiles and lints so the snippets cannot rot silently. // // `go run .` exercises the demo parse call. package main import ( "errors" "log" "github.com/go-openapi/runtime/server-middleware/mediatype" ) // --- Stubs (excluded from rendered snippets) ------------------------ // use silences "declared and not used" diagnostics for snippet-local // values that exist only to make the demo compile. func use(_ ...any) {} func main() { parseMediaType() } // --- Snippets ------------------------------------------------------- func parseMediaType() { // snippet:parseMediaType mt, err := mediatype.Parse("application/json;charset=utf-8;q=0.8") // mt.Type = "application" // mt.Subtype = "json" // mt.Params = {"charset": "utf-8"} // mt.Q = 0.8 if errors.Is(err, mediatype.ErrMalformed) { // ↳ 400 Bad Request territory log.Println("malformed media type") } // endsnippet:parseMediaType use(mt, err) } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/file.go������������������������������������������������������������������0000664�0000000�0000000�00000000731�15202323100�0017324�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import "github.com/go-openapi/swag/fileutils" // File represents an uploaded file. Re-exported from // [fileutils.File] for backwards compatibility. // // See [BindForm] (in form.go) for the orchestrator that parses // multipart / urlencoded request bodies and binds declared file // fields onto handler-side targets. type File = fileutils.File ���������������������������������������go-openapi-runtime-decad8f/file_test.go�������������������������������������������������������������0000664�0000000�0000000�00000001362�15202323100�0020364�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "testing" "github.com/go-openapi/spec" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" "github.com/go-openapi/validate" ) func TestValidateFile(t *testing.T) { fileParam := spec.FileParam("f") validator := validate.NewParamValidator(fileParam, nil) result := validator.Validate("str") require.Len(t, result.Errors, 1) assert.EqualT( t, `f in formData must be of type file: "string"`, result.Errors[0].Error(), ) result = validator.Validate(&File{}) assert.TrueT(t, result.IsValid()) result = validator.Validate(File{}) assert.TrueT(t, result.IsValid()) } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/fixtures/����������������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0017726�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/fixtures/bugs/�����������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0020666�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/fixtures/bugs/172/�������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0021177�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/fixtures/bugs/172/swagger.yml��������������������������������������������0000664�0000000�0000000�00000004076�15202323100�0023370�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������swagger: '2.0' info: version: 1.0.0 title: 'Test' schemes: - http produces: - application/vnd.cia.v1+json paths: /pets: get: description: Returns all pets from the system that the user has access to operationId: findPets parameters: - name: tags in: query description: tags to filter by required: false type: array items: type: string collectionFormat: csv - name: limit in: query description: maximum number of results to return required: false type: integer format: int32 responses: '200': description: pet response schema: type: array items: $ref: '#/definitions/pet' default: description: unexpected error schema: $ref: '#/definitions/errorModel' post: description: Creates a new pet in the store. Duplicates are allowed operationId: addPet consumes: - application/vnd.cia.v1+json parameters: - name: pet in: body description: Pet to add to the store required: true schema: $ref: '#/definitions/newPet' responses: '200': description: pet response schema: $ref: '#/definitions/pet' default: description: unexpected error schema: $ref: '#/definitions/errorModel' definitions: pet: required: - id - name properties: id: type: integer format: int64 name: type: string tag: type: string newPet: allOf: - $ref: '#/definitions/pet' - required: - name properties: id: type: integer format: int64 name: type: string errorModel: required: - code - message properties: code: type: integer format: int32 message: type: string ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/fixtures/bugs/174/�������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0021201�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/fixtures/bugs/174/swagger.yml��������������������������������������������0000664�0000000�0000000�00000004064�15202323100�0023367�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������swagger: '2.0' info: version: 1.0.0 title: 'Test' schemes: - http paths: /pets: get: description: Returns all pets from the system that the user has access to operationId: findPets parameters: - name: tags in: query description: tags to filter by required: false type: array items: type: string collectionFormat: csv - name: limit in: query description: maximum number of results to return required: false type: integer format: int32 responses: '200': description: pet response schema: type: array items: $ref: '#/definitions/pet' default: description: unexpected error schema: $ref: '#/definitions/errorModel' post: description: Creates a new pet in the store. Duplicates are allowed operationId: addPet consumes: - application/json produces: - application/json parameters: - name: pet in: body description: Pet to add to the store required: true schema: $ref: '#/definitions/newPet' responses: '200': description: pet response schema: $ref: '#/definitions/pet' default: description: unexpected error schema: $ref: '#/definitions/errorModel' definitions: pet: required: - id - name properties: id: type: integer format: int64 name: type: string tag: type: string newPet: allOf: - $ref: '#/definitions/pet' - required: - name properties: id: type: integer format: int64 name: type: string errorModel: required: - code - message properties: code: type: integer format: int32 message: type: string ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/fixtures/bugs/264/�������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0021201�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/fixtures/bugs/264/swagger.yml��������������������������������������������0000664�0000000�0000000�00000000511�15202323100�0023360�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������swagger: '2.0' info: version: 1.0.0 title: 'Test' schemes: - http produces: - application/json consumes: - application/json paths: /key/{id}: delete: parameters: - name: id in: path type: integer required: true responses: '200': description: OK ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/flagext/�����������������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0017507�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/flagext/byte_size.go�����������������������������������������������������0000664�0000000�0000000�00000002007�15202323100�0022032�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package flagext import ( "github.com/docker/go-units" ) // ByteSize used to pass byte sizes to a go-flags CLI. type ByteSize int // MarshalFlag implements go-flags Marshaller interface. func (b ByteSize) MarshalFlag() (string, error) { return units.HumanSize(float64(b)), nil } // UnmarshalFlag implements go-flags Unmarshaller interface. func (b *ByteSize) UnmarshalFlag(value string) error { sz, err := units.FromHumanSize(value) if err != nil { return err } *b = ByteSize(int(sz)) return nil } // String method for a bytesize (pflag value and stringer interface). func (b ByteSize) String() string { return units.HumanSize(float64(b)) } // Set the value of this bytesize (pflag value interfaces). func (b *ByteSize) Set(value string) error { return b.UnmarshalFlag(value) } // Type returns the type of the pflag value (pflag value interface). func (b *ByteSize) Type() string { return "byte-size" } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/flagext/byte_size_test.go������������������������������������������������0000664�0000000�0000000�00000001735�15202323100�0023100�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package flagext import ( "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestMarshalBytesize(t *testing.T) { v, err := ByteSize(1024).MarshalFlag() require.NoError(t, err) assert.EqualT(t, "1.024kB", v) } func TestStringBytesize(t *testing.T) { v := ByteSize(2048).String() assert.EqualT(t, "2.048kB", v) } func TestUnmarshalBytesize(t *testing.T) { var b ByteSize err := b.UnmarshalFlag("notASize") require.Error(t, err) err = b.UnmarshalFlag("1MB") require.NoError(t, err) assert.EqualT(t, ByteSize(1000000), b) } func TestSetBytesize(t *testing.T) { var b ByteSize err := b.Set("notASize") require.Error(t, err) err = b.Set("2MB") require.NoError(t, err) assert.EqualT(t, ByteSize(2000000), b) } func TestTypeBytesize(t *testing.T) { var b ByteSize assert.EqualT(t, "byte-size", b.Type()) } �����������������������������������go-openapi-runtime-decad8f/form.go������������������������������������������������������������������0000664�0000000�0000000�00000026172�15202323100�0017357�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( stderrors "errors" "fmt" "mime/multipart" "net/http" "github.com/go-openapi/errors" ) // DefaultMaxUploadFilenameLength is the default cap applied to // FileHeader.Filename for each declared file when [BindForm] is invoked // without an explicit [BindFormMaxFilenameLen] option. // // Multipart headers are allocated per part; an attacker submitting // multi-MB filenames inflates the parser's memory footprint. 1 KiB // matches the IETF guidance for sane filename length and is enough // for realistic uploads. const DefaultMaxUploadFilenameLength = 1024 // DefaultMaxUploadBodySize limits the size of the body to upload forms to 32MB. // // Use an explicit [BindFormMaxBody] option to change this limit. const DefaultMaxUploadBodySize = int64(32) << 20 // filenamePreviewLen caps the byte length of the FileHeader.Filename // preview embedded as the ParseError.Value field when the helper // rejects a too-long filename. const filenamePreviewLen = 32 // ValidateFilenameLength enforces the FileHeader.Filename length cap // that [BindForm] applies via [BindFormFile] declarations. Untyped // binder paths that fetch the file via [http.Request.FormFile] // directly (rather than declaring the file through [BindFormFile]) call // this to opt into the same protection. // // Returns nil if filename length is within maxLen or maxLen <= 0. // Otherwise returns a [*errors.ParseError] suitable for direct return // from a parameter binder. The error embeds a truncated preview of // the offending filename to keep the error message bounded. func ValidateFilenameLength(paramName, paramIn, filename string, maxLen int) error { if maxLen <= 0 || len(filename) <= maxLen { return nil } preview := filename[:min(len(filename), filenamePreviewLen)] return errors.NewParseError(paramName, paramIn, preview, fmt.Errorf("filename length %d exceeds limit %d", len(filename), maxLen)) } // FileBinder is the per-file callback invoked by [BindForm] when a // declared file field is present. // // The callback is responsible for BOTH validating the file (size, MIME, etc.) AND assigning the bound // file to its destination — typically using: // // o.FieldName = &runtime.File{Data: file, Header: header} // // Returning a non-nil error surfaces the error in [BindForm]'s per-field // accumulator. Errors from the binder flow through verbatim — the // binder is expected to produce HTTP-aware errors (e.g. // [errors.ExceedsMaximum] from go-openapi/validate). type FileBinder func(file multipart.File, header *multipart.FileHeader) error // BindOption configures [BindForm]. The variadic style keeps simple // call sites simple and lets new knobs (security caps, additional // behaviour) be added without breaking the signature. type BindOption func(*bindConfig) type bindConfig struct { maxParseMemory int64 maxBody int64 maxFiles int maxFilenameLen int files []formFileSpec } type formFileSpec struct { name string required bool bind FileBinder } // BindFormMaxParseMemory caps the in-memory portion of a multipart // body. Bytes beyond this are spilled to temporary files on disk by // the stdlib parser. 0 (the default) defers to the stdlib's 32 MB. // // This option does NOT cap total body bytes — see [BindFormMaxBody] // for that. The default body cap ([DefaultMaxUploadBodySize] = 32 MB) // is applied even when this option is not supplied, so out of the box // [BindForm] is bounded; callers with stricter or looser requirements // adjust via [BindFormMaxBody]. func BindFormMaxParseMemory(n int64) BindOption { return func(c *bindConfig) { c.maxParseMemory = n } } // BindFormMaxBody caps the size of the body read from a http form before parsing. // // The limit is set to 32MB by default. This default limit is applied for any n=0. // // The limit is disabled for n<0, assuming the caller has already capped the body size upstream. func BindFormMaxBody(n int64) BindOption { return func(c *bindConfig) { c.maxBody = n } } // BindFormMaxFiles rejects parses where the total number of file // parts across all field names exceeds n. 0 (the default) means no // cap. Exceeding the cap is a fatal error — [BindForm] returns // fatal=true and no per-file binders run. func BindFormMaxFiles(n int) BindOption { return func(c *bindConfig) { c.maxFiles = n } } // BindFormMaxFilenameLen rejects per-file headers whose Filename // length exceeds n. 0 means no cap; the default applied when this // option is not supplied is [DefaultMaxUploadFilenameLength]. The // cap is a per-field bind error (non-fatal); other declared files // still run. func BindFormMaxFilenameLen(n int) BindOption { return func(c *bindConfig) { c.maxFilenameLen = n } } // BindFormFile declares a file field to bind under the given form // name. If required is true and the field is absent, [BindForm] // produces the per-field error. // // errors.NewParseError(name, "formData", "", http.ErrMissingFile) // // If required is false, absence is silent (no error, no bind). // // The bind callback runs only when the field is present. It is the // site where both validation and assignment happen — see [FileBinder]. // // FileHeader.Filename is attacker-controlled text; the binder MUST // NOT use it directly as a filesystem path. The helper does not // touch the filesystem. func BindFormFile(name string, required bool, bind FileBinder) BindOption { return func(c *bindConfig) { c.files = append(c.files, formFileSpec{ name: name, required: required, bind: bind, }) } } // BindForm parses r as multipart/form-data, falling back to // application/x-www-form-urlencoded when the request is not // multipart. On success, r.MultipartForm and r.PostForm are populated; // the caller can read non-file form values via [Values](r.Form) after // the call returns. // // All errors produced by BindForm itself (parse failure, missing // required field, cap exceeded) are [*errors.ParseError] values built // via [errors.NewParseError], matching the untyped // middleware/parameter.go path. Errors returned by per-file binders // flow through verbatim — binders own their HTTP-aware error shape. // // Per-file binders declared via [BindFormFile] run in declaration // order after a successful parse. Their errors are accumulated and // returned wrapped in [errors.CompositeValidationError]; the caller // typically appends the returned err to its own []error and continues // with non-file parameter binding. // // Return semantics: // // - fatal=true, err!=nil: parse failure or a hard cap (e.g. // [BindFormMaxFiles]) was exceeded. No per-file binders ran; the // caller MUST return err immediately. // - fatal=false, err!=nil: one or more per-file binders produced // errors. The form parsed successfully; r.Form is populated. The // caller appends err to its accumulator and continues. // - fatal=false, err==nil: full success. // // fatal==true implies err!=nil. // // Defaults applied out of the box: // // - Total body bytes capped at [DefaultMaxUploadBodySize] (32 MB) // via [http.MaxBytesReader]. Adjust with [BindFormMaxBody] // (negative n disables, when the caller has already capped the // body upstream). // - FileHeader.Filename length capped at // [DefaultMaxUploadFilenameLength]. Adjust with // [BindFormMaxFilenameLen]. // // Caller responsibilities the helper does NOT cover: // // - Set [http.Server.ReadTimeout] / [http.Server.IdleTimeout] to defend // against slow-read attacks. // - Decompress Content-Encoding: gzip request bodies upstream if // the API accepts them, using a size-limited reader. // - Treat FileHeader.Filename as untrusted user input; never use // it directly as a filesystem path. func BindForm(r *http.Request, opts ...BindOption) (fatal bool, err error) { cfg := bindConfig{ maxFilenameLen: DefaultMaxUploadFilenameLength, } for _, opt := range opts { opt(&cfg) } if perr := parseFormBody(r, cfg.maxParseMemory, cfg.maxBody); perr != nil { // Body-cap hit gets the 413 status; everything else maps to a // 400 ParseError. parseFormBody returns the raw stdlib error // in both cases — the HTTP-aware wrapping happens here. var maxBytesErr *http.MaxBytesError if stderrors.As(perr, &maxBytesErr) { return true, errors.New(http.StatusRequestEntityTooLarge, "formData: %v", perr) } return true, errors.NewParseError("body", "formData", "", perr) } if cfg.maxFiles > 0 { if got := countFileParts(r); got > cfg.maxFiles { return true, errors.NewParseError("body", "formData", "", fmt.Errorf("multipart form contains %d file parts, exceeds limit %d", got, cfg.maxFiles)) } } var bindErrs []error for _, spec := range cfg.files { if e := bindFormFile(r, spec, cfg.maxFilenameLen); e != nil { bindErrs = append(bindErrs, e) } } if len(bindErrs) > 0 { return false, errors.CompositeValidationError(bindErrs...) } return false, nil } // parseFormBody parses the request body. Content-Type drives the // parser: multipart/form-data → r.ParseMultipartForm, everything else // → r.ParseForm (stdlib's parsePostForm only actually reads the body // when Content-Type is application/x-www-form-urlencoded, so calling // ParseForm is safe for unrecognised types). // // Caveat: ParseMultipartForm calls ParseForm internally and discards its error // when the body turns out not to be multipart, returning ErrNotMultipart instead // — the subsequent retry then short-circuits because r.PostForm is already // set. Content-type-based routing avoids the lossy detour. // // Returns the raw stdlib error on failure; the caller (BindForm) // handles HTTP-aware wrapping (413 for MaxBytesError, 400 ParseError // otherwise). // // maxMemory == 0 falls through to the stdlib default (32 MB). // maxBody == 0 defaults to DefaultMaxUploadBodySize; maxBody < 0 // disables the body cap (caller has capped upstream). func parseFormBody(r *http.Request, maxMemory, maxBody int64) error { if r.Body != nil && maxBody >= 0 { if maxBody == 0 { maxBody = DefaultMaxUploadBodySize } r.Body = http.MaxBytesReader(nil, r.Body, maxBody) } mt, _, _ := ContentType(r.Header) if mt == MultipartFormMime { //nolint:gosec // G120: false positive -- see below // gosec doesn't track the Body. // See https://github.com/securego/gosec/blob/de65614d10a6b84029e3e1215567b8ce7e490f23/testutils/g120_samples.go#L57 return r.ParseMultipartForm(maxMemory) } return r.ParseForm() } func countFileParts(r *http.Request) int { if r.MultipartForm == nil { return 0 } var n int for _, fhs := range r.MultipartForm.File { n += len(fhs) } return n } func bindFormFile(r *http.Request, spec formFileSpec, maxFilenameLen int) error { file, header, err := r.FormFile(spec.name) if err != nil { if stderrors.Is(err, http.ErrMissingFile) { if spec.required { return errors.New(http.StatusBadRequest, "formData: %v", http.ErrMissingFile) } return nil } return errors.NewParseError(spec.name, "formData", "", err) } if err := ValidateFilenameLength(spec.name, "formData", header.Filename, maxFilenameLen); err != nil { return err } if spec.bind == nil { return nil } return spec.bind(file, header) } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/form_fuzz_test.go��������������������������������������������������������0000664�0000000�0000000�00000007750�15202323100�0021475�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "bytes" "fmt" "mime/multipart" "net/http" "net/http/httptest" "strings" "testing" ) // FuzzBindForm exercises the full BindForm parse path with arbitrary // multipart bodies. Invariants: must not panic, hang, or return // fatal=true alongside err=nil. // // Security scrub Lens 3 / L3.8 — fuzz coverage for the form-binding // surface. func FuzzBindForm(f *testing.F) { const boundary = "FUZZBOUND" ct := "multipart/form-data; boundary=" + boundary seeds := [][]byte{ // Well-formed single text part. []byte("--" + boundary + "\r\n" + `Content-Disposition: form-data; name="x"` + "\r\n\r\n" + "v\r\n" + "--" + boundary + "--\r\n"), // Well-formed single file part. []byte("--" + boundary + "\r\n" + `Content-Disposition: form-data; name="f"; filename="t.txt"` + "\r\n" + "Content-Type: text/plain\r\n\r\n" + "data\r\n" + "--" + boundary + "--\r\n"), // Empty body. nil, // Only the closing boundary. []byte("--" + boundary + "--\r\n"), // Truncated body (no closing boundary). []byte("--" + boundary + "\r\n" + `Content-Disposition: form-data; name="x"` + "\r\n\r\n"), // Adversarial filename (long). []byte("--" + boundary + "\r\n" + `Content-Disposition: form-data; name="f"; filename="` + strings.Repeat("a", 4096) + `"` + "\r\n\r\n" + "x\r\n" + "--" + boundary + "--\r\n"), // Garbage that doesn't start with a boundary. []byte("not-a-multipart-body"), } for _, s := range seeds { f.Add(s) } f.Fuzz(func(t *testing.T, body []byte) { r := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", bytes.NewReader(body)) r.Header.Set("Content-Type", ct) fatal, err := BindForm(r, BindFormFile("f", false, func(_ multipart.File, _ *multipart.FileHeader) error { return nil }), ) if fatal && err == nil { t.Fatalf("BindForm returned fatal=true with err=nil for body %q", body) } }) } // FuzzBindFormFilename targets the filename-cap path specifically. // It feeds an arbitrary filename through a synthetic well-formed // multipart body and asserts the bound *FileHeader.Filename length // never exceeds DefaultMaxUploadFilenameLength. // // Security scrub Lens 3 / L3.1 + L3.8. func FuzzBindFormFilename(f *testing.F) { seeds := []string{ "normal.txt", "", strings.Repeat("a", DefaultMaxUploadFilenameLength), // exactly at cap strings.Repeat("a", DefaultMaxUploadFilenameLength+1), // one over strings.Repeat("a", DefaultMaxUploadFilenameLength*100), // way over "a/b/c.txt", "../etc/passwd", "\x00", "file\r\nContent-Type: forged", string([]byte{0xff, 0xfe}), } for _, s := range seeds { f.Add(s) } const boundary = "FUZZBOUND" f.Fuzz(func(t *testing.T, filename string) { // Build a well-formed multipart wrapper around the fuzzed // filename. %q quote-escapes so the wire stays parseable; // the bytes BindForm sees as Filename are the same fuzz // input after the stdlib parser decodes the quoted-string. body := fmt.Sprintf( "--%s\r\n"+ `Content-Disposition: form-data; name="f"; filename=%q`+"\r\n"+ "Content-Type: application/octet-stream\r\n"+ "\r\n"+ "data\r\n"+ "--%s--\r\n", boundary, filename, boundary, ) r := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", strings.NewReader(body)) r.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary) var bound *multipart.FileHeader fatal, err := BindForm(r, BindFormFile("f", false, func(_ multipart.File, h *multipart.FileHeader) error { bound = h return nil }), ) if fatal && err == nil { t.Fatalf("fatal=true with err=nil for filename %q", filename) } if err == nil && bound != nil { if len(bound.Filename) > DefaultMaxUploadFilenameLength { t.Fatalf("BindForm bound a file with filename length %d > cap %d (filename=%q)", len(bound.Filename), DefaultMaxUploadFilenameLength, bound.Filename) } } }) } ������������������������go-openapi-runtime-decad8f/form_test.go�������������������������������������������������������������0000664�0000000�0000000�00000034265�15202323100�0020420�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "bytes" stderrors "errors" "io" "mime/multipart" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-openapi/errors" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) const ( testUploadPath = "/upload" testFileFieldB = "b.txt" testFileFieldOK = "ok.txt" testFileFieldA = "a.txt" testFileData = "data" testFieldDesc = "desc" testFieldFile = "file" testFieldFile1 = "file1" testFieldFile2 = "file2" testValueHello = "hello" ) // multipartBody builds a multipart/form-data body with the given file // parts ({name, filename, content}) and form values. Returns the body // bytes and the Content-Type header to set on the request. type multipartFile struct { field string filename string content string } func multipartBody(t *testing.T, files []multipartFile, values map[string]string) (*bytes.Buffer, string) { t.Helper() buf := &bytes.Buffer{} w := multipart.NewWriter(buf) for _, f := range files { fw, err := w.CreateFormFile(f.field, f.filename) require.NoError(t, err) _, err = io.WriteString(fw, f.content) require.NoError(t, err) } for k, v := range values { require.NoError(t, w.WriteField(k, v)) } require.NoError(t, w.Close()) return buf, w.FormDataContentType() } func newMultipartRequest(t *testing.T, files []multipartFile, values map[string]string) *http.Request { t.Helper() body, ct := multipartBody(t, files, values) r := httptest.NewRequestWithContext(t.Context(), http.MethodPost, testUploadPath, body) r.Header.Set("Content-Type", ct) return r } func newURLEncodedRequest(t *testing.T, values map[string]string) *http.Request { t.Helper() form := make([]string, 0, len(values)) for k, v := range values { form = append(form, k+"="+v) } r := httptest.NewRequestWithContext(t.Context(), http.MethodPost, testUploadPath, strings.NewReader(strings.Join(form, "&"))) r.Header.Set("Content-Type", URLencodedFormMime) return r } // assertParseError extracts a *errors.ParseError from err and checks // its name/in fields plus an optional reason predicate. func assertParseError(t *testing.T, err error, wantName string, reasonCheck func(error) bool) { t.Helper() require.Error(t, err) var pe *errors.ParseError require.True(t, stderrors.As(err, &pe), "expected *errors.ParseError, got %T", err) assert.EqualT(t, wantName, pe.Name) assert.EqualT(t, "formData", pe.In) if reasonCheck != nil { require.True(t, reasonCheck(pe.Reason), "reason check failed for %v", pe.Reason) } } // assertCompositeContains extracts a *errors.CompositeError from err // and asserts that at least n inner errors satisfy match. // //nolint:unparam // left variable n for future assertions func assertCompositeContains(t *testing.T, err error, n int, match func(error) bool) { t.Helper() require.Error(t, err) var ce *errors.CompositeError require.True(t, stderrors.As(err, &ce), "expected *errors.CompositeError, got %T", err) var got int for _, e := range ce.Errors { if match(e) { got++ } } assert.EqualT(t, n, got, "matched %d inner errors, want %d", got, n) } func TestBindForm_parseOnly_multipart(t *testing.T) { r := newMultipartRequest(t, nil, map[string]string{testFieldDesc: testValueHello}) fatal, err := BindForm(r) assert.FalseT(t, fatal) require.NoError(t, err) require.NotNil(t, r.MultipartForm) assert.EqualT(t, testValueHello, r.Form.Get(testFieldDesc)) } func TestBindForm_parseOnly_urlencoded(t *testing.T) { r := newURLEncodedRequest(t, map[string]string{testFieldDesc: testValueHello, "count": "42"}) fatal, err := BindForm(r) assert.FalseT(t, fatal) require.NoError(t, err) assert.EqualT(t, testValueHello, r.PostForm.Get(testFieldDesc)) assert.EqualT(t, "42", r.PostForm.Get("count")) } func TestBindForm_parseFailure_urlencoded(t *testing.T) { // Malformed URL escape in an urlencoded body. An earlier draft routed // through ParseMultipartForm which silently swallowed this class of // errors; this test guards against the regression. r := httptest.NewRequestWithContext(t.Context(), http.MethodPost, testUploadPath, strings.NewReader("name=%3&age=32")) r.Header.Set("Content-Type", URLencodedFormMime) fatal, err := BindForm(r) assert.TrueT(t, fatal) assertParseError(t, err, "body", nil) } func TestBindForm_parseFailure(t *testing.T) { // Multipart Content-Type but truncated body — ParseMultipartForm fails. body, ct := multipartBody(t, []multipartFile{{testFieldFile, "f.txt", testValueHello}}, nil) truncated := body.Bytes()[:len(body.Bytes())-5] r := httptest.NewRequestWithContext(t.Context(), http.MethodPost, testUploadPath, bytes.NewReader(truncated)) r.Header.Set("Content-Type", ct) fatal, err := BindForm(r) assert.TrueT(t, fatal) assertParseError(t, err, "body", nil) } func TestBindForm_singleRequired_present(t *testing.T) { r := newMultipartRequest(t, []multipartFile{{testFieldFile, "hello.txt", testValueHello}}, nil) var bound *File fatal, err := BindForm(r, BindFormFile(testFieldFile, true, func(f multipart.File, h *multipart.FileHeader) error { bound = &File{Data: f, Header: h} return nil })) assert.FalseT(t, fatal) require.NoError(t, err) require.NotNil(t, bound) assert.EqualT(t, "hello.txt", bound.Header.Filename) } func TestBindForm_singleRequired_missing(t *testing.T) { r := newMultipartRequest(t, nil, map[string]string{testFieldDesc: "x"}) called := false fatal, err := BindForm(r, BindFormFile(testFieldFile, true, func(_ multipart.File, _ *multipart.FileHeader) error { called = true return nil })) assert.FalseT(t, fatal) assert.FalseT(t, called) assertCompositeContains(t, err, 1, func(e error) bool { var apiErr errors.Error if !stderrors.As(e, &apiErr) { return false } return apiErr.Code() == http.StatusBadRequest && strings.Contains(apiErr.Error(), http.ErrMissingFile.Error()) }) } func TestBindForm_optional_missing(t *testing.T) { r := newMultipartRequest(t, nil, map[string]string{testFieldDesc: "x"}) called := false fatal, err := BindForm(r, BindFormFile(testFieldFile, false, func(_ multipart.File, _ *multipart.FileHeader) error { called = true return nil })) assert.FalseT(t, fatal) require.NoError(t, err) assert.FalseT(t, called, "optional missing file should not invoke binder") } func TestBindForm_optional_present(t *testing.T) { r := newMultipartRequest(t, []multipartFile{{testFieldFile, "hi.txt", "hi"}}, nil) called := false fatal, err := BindForm(r, BindFormFile(testFieldFile, false, func(f multipart.File, h *multipart.FileHeader) error { called = true assert.EqualT(t, "hi.txt", h.Filename) got, _ := io.ReadAll(f) assert.EqualT(t, "hi", string(got)) return nil })) assert.FalseT(t, fatal) require.NoError(t, err) assert.TrueT(t, called) } func TestBindForm_mixed_files_and_values(t *testing.T) { r := newMultipartRequest(t, []multipartFile{ {testFieldFile1, testFileFieldA, "AAA"}, {testFieldFile2, testFileFieldB, "BBBB"}, }, map[string]string{testFieldDesc: "two files", "count": "2"}, ) var f1, f2 *File fatal, err := BindForm(r, BindFormFile(testFieldFile1, true, func(f multipart.File, h *multipart.FileHeader) error { f1 = &File{Data: f, Header: h} return nil }), BindFormFile(testFieldFile2, false, func(f multipart.File, h *multipart.FileHeader) error { f2 = &File{Data: f, Header: h} return nil }), ) assert.FalseT(t, fatal) require.NoError(t, err) require.NotNil(t, f1) require.NotNil(t, f2) assert.EqualT(t, testFileFieldA, f1.Header.Filename) assert.EqualT(t, testFileFieldB, f2.Header.Filename) assert.EqualT(t, "two files", r.Form.Get(testFieldDesc)) assert.EqualT(t, "2", r.Form.Get("count")) } func TestBindForm_binderError(t *testing.T) { r := newMultipartRequest(t, []multipartFile{{testFieldFile, "f.txt", testFileData}}, nil) sentinel := stderrors.New("binder rejected") fatal, err := BindForm(r, BindFormFile(testFieldFile, true, func(_ multipart.File, _ *multipart.FileHeader) error { return sentinel })) assert.FalseT(t, fatal) assertCompositeContains(t, err, 1, func(e error) bool { return stderrors.Is(e, sentinel) }) } func TestBindForm_multipleBinderErrors(t *testing.T) { r := newMultipartRequest(t, []multipartFile{ {testFieldFile1, testFileFieldA, "A"}, {testFieldFile2, testFileFieldB, "B"}, }, nil, ) errA := stderrors.New("A failed") errB := stderrors.New("B failed") fatal, err := BindForm(r, BindFormFile(testFieldFile1, true, func(_ multipart.File, _ *multipart.FileHeader) error { return errA }), BindFormFile(testFieldFile2, true, func(_ multipart.File, _ *multipart.FileHeader) error { return errB }), ) assert.FalseT(t, fatal) assertCompositeContains(t, err, 1, func(e error) bool { return stderrors.Is(e, errA) }) assertCompositeContains(t, err, 1, func(e error) bool { return stderrors.Is(e, errB) }) } func TestBindForm_maxFiles_exceeded(t *testing.T) { r := newMultipartRequest(t, []multipartFile{ {testFieldFile1, testFileFieldA, "A"}, {testFieldFile2, testFileFieldB, "B"}, {"file3", "c.txt", "C"}, }, nil, ) bound := 0 fatal, err := BindForm(r, BindFormMaxFiles(2), BindFormFile(testFieldFile1, false, func(_ multipart.File, _ *multipart.FileHeader) error { bound++ return nil }), ) assert.TrueT(t, fatal) assertParseError(t, err, "body", nil) assert.EqualT(t, 0, bound, "no binders should run after maxFiles exceeded") } func TestBindForm_maxFilenameLen_exceeded(t *testing.T) { longName := strings.Repeat("x", 50) + ".txt" r := newMultipartRequest(t, []multipartFile{ {"big", longName, testFileData}, {"small", testFileFieldOK, testFileData}, }, nil, ) smallBound := false fatal, err := BindForm(r, BindFormMaxFilenameLen(10), BindFormFile("big", true, func(_ multipart.File, _ *multipart.FileHeader) error { return nil }), BindFormFile("small", false, func(_ multipart.File, _ *multipart.FileHeader) error { smallBound = true return nil }), ) assert.FalseT(t, fatal) assertCompositeContains(t, err, 1, func(e error) bool { var pe *errors.ParseError if !stderrors.As(e, &pe) { return false } return pe.Name == "big" }) assert.TrueT(t, smallBound, "small file should still bind after big rejected") } func TestBindForm_maxMemory_zero(t *testing.T) { r := newMultipartRequest(t, []multipartFile{{testFieldFile, testFileFieldOK, testValueHello}}, nil) fatal, err := BindForm(r, BindFormMaxParseMemory(0), BindFormFile(testFieldFile, true, func(_ multipart.File, _ *multipart.FileHeader) error { return nil }), ) assert.FalseT(t, fatal) require.NoError(t, err) } func TestBindForm_maxBody_underCap(t *testing.T) { r := newMultipartRequest(t, []multipartFile{{testFieldFile, testFileFieldOK, testFileData}}, nil) fatal, err := BindForm(r, BindFormMaxBody(1<<20), BindFormFile(testFieldFile, true, func(_ multipart.File, _ *multipart.FileHeader) error { return nil }), ) assert.FalseT(t, fatal) require.NoError(t, err) } func TestBindForm_maxBody_overCap(t *testing.T) { r := newMultipartRequest(t, []multipartFile{{testFieldFile, testFileFieldOK, strings.Repeat("x", 2048)}}, nil) fatal, err := BindForm(r, BindFormMaxBody(256), BindFormFile(testFieldFile, true, func(_ multipart.File, _ *multipart.FileHeader) error { return nil }), ) assert.TrueT(t, fatal) require.Error(t, err) var apiErr errors.Error require.True(t, stderrors.As(err, &apiErr), "expected errors.Error, got %T", err) assert.EqualT(t, int32(http.StatusRequestEntityTooLarge), apiErr.Code()) } func TestBindForm_maxBody_disabled(t *testing.T) { // Body well above DefaultMaxUploadBodySize would be expensive; just // confirm a 2 MiB body parses with n=-1 (disabled) when the implicit // default would otherwise stay at 32 MiB anyway. The point is that // passing -1 doesn't itself break parsing. r := newMultipartRequest(t, []multipartFile{{testFieldFile, testFileFieldOK, strings.Repeat("x", 2<<20)}}, nil) fatal, err := BindForm(r, BindFormMaxBody(-1), BindFormFile(testFieldFile, true, func(_ multipart.File, _ *multipart.FileHeader) error { return nil }), ) assert.FalseT(t, fatal) require.NoError(t, err) } func TestBindForm_idempotent(t *testing.T) { r := newMultipartRequest(t, []multipartFile{{testFieldFile, testFileFieldOK, testValueHello}}, map[string]string{testFieldDesc: "x"}) fatal1, err1 := BindForm(r, BindFormFile(testFieldFile, true, func(_ multipart.File, _ *multipart.FileHeader) error { return nil })) require.NoError(t, err1) assert.FalseT(t, fatal1) // Re-call: stdlib short-circuits because r.MultipartForm != nil. fatal2, err2 := BindForm(r, BindFormFile(testFieldFile, true, func(_ multipart.File, _ *multipart.FileHeader) error { return nil })) require.NoError(t, err2) assert.FalseT(t, fatal2) assert.EqualT(t, "x", r.Form.Get(testFieldDesc)) } // TestValidateFilenameLength covers the exported helper used by both // BindForm's BindFormFile path and the untyped middleware/parameter.go // formData path. Security scrub Lens 3 / L3.1. func TestValidateFilenameLength(t *testing.T) { t.Run("within cap returns nil", func(t *testing.T) { require.NoError(t, ValidateFilenameLength("avatar", "formData", "ok.txt", 1024)) }) t.Run("at cap returns nil", func(t *testing.T) { name := strings.Repeat("x", 10) require.NoError(t, ValidateFilenameLength("avatar", "formData", name, 10)) }) t.Run("over cap returns ParseError", func(t *testing.T) { name := strings.Repeat("x", 50) err := ValidateFilenameLength("avatar", "formData", name, 10) require.Error(t, err) var pe *errors.ParseError require.True(t, stderrors.As(err, &pe)) assert.EqualT(t, "avatar", pe.Name) assert.EqualT(t, "formData", pe.In) }) t.Run("preview is truncated", func(t *testing.T) { name := strings.Repeat("y", 200) err := ValidateFilenameLength("avatar", "formData", name, 10) require.Error(t, err) var pe *errors.ParseError require.True(t, stderrors.As(err, &pe)) // preview must fit filenamePreviewLen (32 bytes). assert.LessOrEqual(t, len(pe.Value), filenamePreviewLen) }) t.Run("maxLen<=0 disables the cap", func(t *testing.T) { name := strings.Repeat("z", 10000) require.NoError(t, ValidateFilenameLength("avatar", "formData", name, 0)) require.NoError(t, ValidateFilenameLength("avatar", "formData", name, -1)) }) } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/go.mod�������������������������������������������������������������������0000664�0000000�0000000�00000003410�15202323100�0017161�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������module github.com/go-openapi/runtime require ( github.com/docker/go-units v0.5.0 github.com/go-openapi/analysis v0.25.0 github.com/go-openapi/errors v0.22.7 github.com/go-openapi/loads v0.23.3 github.com/go-openapi/runtime/server-middleware v0.30.0 github.com/go-openapi/spec v0.22.4 github.com/go-openapi/strfmt v0.26.2 github.com/go-openapi/swag/conv v0.26.0 github.com/go-openapi/swag/fileutils v0.26.0 github.com/go-openapi/swag/jsonutils v0.26.0 github.com/go-openapi/swag/stringutils v0.26.0 github.com/go-openapi/swag/typeutils v0.26.0 github.com/go-openapi/testify/enable/yaml/v2 v2.5.0 github.com/go-openapi/testify/v2 v2.5.0 github.com/go-openapi/validate v0.25.2 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/sync v0.20.0 ) replace github.com/go-openapi/runtime/server-middleware => ./server-middleware require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/swag/jsonname v0.26.0 // indirect github.com/go-openapi/swag/loading v0.26.0 // indirect github.com/go-openapi/swag/mangling v0.26.0 // indirect github.com/go-openapi/swag/yamlutils v0.26.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect golang.org/x/net v0.54.0 // indirect golang.org/x/sys v0.44.0 // indirect golang.org/x/text v0.37.0 // indirect ) go 1.25.0 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/go.sum�������������������������������������������������������������������0000664�0000000�0000000�00000021176�15202323100�0017217�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs= github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/strfmt v0.26.2 h1:ysjheCh4i1rmFEo2LanhELDNucNzfWTZhUDKgWWPaFM= github.com/go-openapi/strfmt v0.26.2/go.mod h1:fXh1e449cyUn2NYuz+wb3wARBUdMl7qPEZwX00nqivY= github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= github.com/go-openapi/testify/enable/yaml/v2 v2.5.0 h1:3hZD1fwydvCx/cc1R2uYNQirHqf2s6lqpKV3FcNTURA= github.com/go-openapi/testify/enable/yaml/v2 v2.5.0/go.mod h1:TvDZKBH7ZbMaF3EqH2AwTvNQCmzyZq8K1agRjf1B+Nk= github.com/go-openapi/testify/v2 v2.5.0 h1:UOCr63aAsMIDydZbZGqo5Ev01D4eydItRbekDuZMJLw= github.com/go-openapi/testify/v2 v2.5.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/go.work������������������������������������������������������������������0000664�0000000�0000000�00000000135�15202323100�0017365�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use ( . ./client-middleware/opentracing ./docs/examples ./server-middleware ) go 1.25.0 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/��������������������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0016763�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/.gitkeep������������������������������������������������������������0000664�0000000�0000000�00000000000�15202323100�0020402�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/�����������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0020472�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0021434�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/.gitignore��������������������������������������������0000664�0000000�0000000�00000000033�15202323100�0023420�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������public *.lock runtime.yaml �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/README.md���������������������������������������������0000664�0000000�0000000�00000004127�15202323100�0022717�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Hugo documentation site This directory holds the Hugo configuration that builds <https://go-openapi.github.io/runtime/>. ## Layout ```text hugo/ ├── hugo.yaml # Static Hugo config ├── runtime.yaml.template # Build-time config template (version info) ├── runtime.yaml # Generated from the template (git-ignored output) ├── gendoc.go # Local development helper (`go run gendoc.go`) ├── themes/ │ ├── hugo-relearn/ # Relearn theme (downloaded by CI / dev script) │ ├── runtime-assets/ # Custom logo / SCSS │ └── runtime-static/ # Static branding (favicon, …) └── layouts/ ├── shortcodes/ # Custom Hugo shortcodes └── partials/ # Custom partial templates ``` ## Content Markdown content is mounted from `../../../docs/doc-site/` via the `module.mounts` block in `hugo.yaml`. Editing those files (or adding new ones) is enough — no codegen or generator is involved. ## Local preview ```sh go run gendoc.go ``` The script: 1. Extracts version info from git tags and the root `go.mod` 2. Renders `runtime.yaml` from `runtime.yaml.template` 3. Starts `hugo server` on <http://localhost:1313/runtime/> with live reload Requires `hugo` (extended, ≥ v0.150) and `git` on `PATH`. ## Configuration Two-layer config, mirroring the pattern used by other go-openapi doc sites: 1. **`hugo.yaml`** — static configuration (theme, mounts, menu, params) 2. **`runtime.yaml`** — dynamic configuration (Go version, latest release tag, build timestamp), generated from `runtime.yaml.template` Both files are passed together via `--config hugo.yaml,runtime.yaml`. The dynamic values land under `params.runtime.*` and are referenced from the markdown content. ## Deployment GitHub Actions workflow `.github/workflows/update-doc.yml`: - Builds on every push to `master` and on tags `v*` that touch `docs/**`, `hack/doc-site/**`, or the workflow itself - Publishes the rendered site to GitHub Pages (<https://go-openapi.github.io/runtime/>) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/gendoc.go���������������������������������������������0000664�0000000�0000000�00000007646�15202323100�0023237�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//go:build ignore // Local development script for Hugo documentation. // // Usage: go run gendoc.go // // Requires: // * hugo // * git package main import ( "bufio" "context" "fmt" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" "time" ) //nolint:forbidigo,dogsled func main() { ctx := context.Background() // Change to the directory containing this script. _, thisFile, _, _ := runtime.Caller(0) scriptDir := filepath.Dir(thisFile) if err := os.Chdir(scriptDir); err != nil { fatalf("chdir: %v", err) } fmt.Println("==> Preparing Hugo documentation site...") latestRelease := gitLatestRelease(ctx) requiredGoVersion := goVersionFromMod() buildTime := time.Now().UTC().Format("2006-01-02T15:04:05Z") versionMessage := "Documentation test for latest master" fmt.Printf(" Latest release: %s\n", latestRelease) fmt.Printf(" Go version: %s\n", requiredGoVersion) fmt.Printf(" Build time: %s\n", buildTime) // Generate dynamic config from template. generateRuntimeYAML(requiredGoVersion, latestRelease, versionMessage, buildTime) fmt.Println("==> Generated runtime.yaml") // Check if theme exists. if _, err := os.Stat("themes/hugo-relearn"); os.IsNotExist(err) { fatalf("Relearn theme not found at themes/hugo-relearn\n" + "Run: unzip hugo-theme-relearn-main.zip -d themes/ && mv themes/hugo-theme-relearn-main themes/hugo-relearn") } // Check if generated docs exist. if _, err := os.Stat("../../../docs/doc-site"); os.IsNotExist(err) { fmt.Println("WARNING: Generated docs not found at ../../../docs/doc-site") fmt.Println("You may need to run: go generate ./...") fmt.Println() fmt.Println("Creating placeholder content directory...") os.MkdirAll("content", 0o755) //nolint:errcheck,mnd } fmt.Println("==> Starting Hugo development server...") fmt.Println(" Visit: http://localhost:1313/runtime/") fmt.Println() // Start Hugo server with both configs. cmd := exec.CommandContext(ctx, "hugo", "server", "--config", "hugo.yaml,runtime.yaml", "--buildDrafts", "--disableFastRender", "--navigateToChanged", "--bind", "0.0.0.0", "--port", "1313", "--baseURL", "http://localhost:1313/runtime/", "--appendPort=false", "--logLevel", "info", "--cleanDestinationDir", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin if err := cmd.Run(); err != nil { fatalf("hugo: %v", err) } } // gitLatestRelease returns the latest semver tag, or "dev" if none found. func gitLatestRelease(ctx context.Context) string { out, err := exec.CommandContext(ctx, "git", "tag", "--list", "--sort", "-version:refname", "v*").Output() if err != nil || len(out) == 0 { return "dev" } sc := bufio.NewScanner(strings.NewReader(string(out))) if sc.Scan() { return strings.TrimSpace(sc.Text()) } return "dev" } // goVersionFromMod extracts the go version from the root go.mod. func goVersionFromMod() string { data, err := os.ReadFile("../../../go.mod") if err != nil { fatalf("reading go.mod: %v", err) } re := regexp.MustCompile(`(?m)^go\s+(\S+)`) m := re.FindSubmatch(data) if m == nil { fatalf("could not find go version in go.mod") } return string(m[1]) } // generateRuntimeYAML reads the template and writes runtime.yaml with substitutions. func generateRuntimeYAML(goVersion, latestRelease, versionMessage, buildTime string) { tmpl, err := os.ReadFile("runtime.yaml.template") if err != nil { fatalf("reading template: %v", err) } out := string(tmpl) out = strings.ReplaceAll(out, "{{ GO_VERSION }}", goVersion) out = strings.ReplaceAll(out, "{{ LATEST_RELEASE }}", latestRelease) out = strings.ReplaceAll(out, "{{ VERSION_MESSAGE }}", versionMessage) out = strings.ReplaceAll(out, "{{ BUILD_TIME }}", buildTime) if err := os.WriteFile("runtime.yaml", []byte(out), 0o600); err != nil { //nolint:mnd fatalf("writing runtime.yaml: %v", err) } } func fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, "ERROR: "+format+"\n", args...) os.Exit(1) } ������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/hugo.yaml���������������������������������������������0000664�0000000�0000000�00000006064�15202323100�0023270�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������baseURL: https://go-openapi.github.io/runtime/ title: go-openapi runtime theme: hugo-relearn languageCode: en-us # Enable features enableEmoji: true enableGitInfo: true enableRobotsTXT: true # Content configuration contentDir: content # Output formats outputs: home: - html - rss - print # Markup configuration markup: goldmark: renderer: unsafe: true # Allow raw HTML in markdown parser: attribute: title: true block: true renderHooks: image: useEmbedded: always highlight: codeFences: true guessSyntax: false lineNos: false lineNumbersInTable: false noClasses: true style: monokai tabWidth: 4 # Taxonomies are disabled for now disableKinds: - taxonomy - term # Module mounts # Following go-swagger pattern: simple mounts for content and assets module: mounts: # Custom layouts (override theme if needed) - source: layouts target: layouts # Generated documentation content - source: '../../../docs/doc-site' target: content # Runnable Go examples surfaced via the `code` shortcode. # Mount source files plus a few resource extensions used by `//go:embed` # fixtures (e.g. swagger.json, openapi.yaml). go.mod / go.sum stay out. - source: '../../../docs/examples' target: assets/examples files: - '**/*.go' - '**/*.json' - '**/*.yaml' - '**/*.yml' # Custom SCSS (for theme customization) + logo - source: themes/runtime-assets target: assets # Custom static files (favicon) - source: themes/runtime-static target: static # Relearn theme parameters params: editURL: 'https://github.com/go-openapi/runtime/edit/master/' externalLinkTarget: _blank # Repository info sourceRepository: 'https://github.com/go-openapi/runtime' # Branding author: name: 'go-openapi maintainers' hideAuthorEmail: true # Theme customization themeVariant: - zen-dark - relearn-dark - relearn-light showVisitedLinks: true collapsibleMenu: true disableBreadcrumb: false disableNextPrev: false disableLandingPageButton: true titleSeparator: '|' # Features search: disable: false index: disable: false page: disable: false adapter: identifier: lunr disableLanguageSwitchingButton: true disableInlineCopyToClipBoard: false # Menu ordering ordersectionsby: 'weight' # Proposal for enhancement: configure versions #versions: #- baseURL: https://go-openapi.github.io/runtime/ # identifier: v2.1.0 # isLatest: true #version: 'v2.1.0' # Custom params runtime: goVersion: '' latestRelease: '' versionMessage: '' buildTime: '' # Menu configuration menu: shortcuts: - name: "GitHub" identifier: github url: "https://github.com/go-openapi/runtime" weight: 10 - name: "go-openapi toolkit" identifier: go-openapi url: "https://github.com/go-openapi" weight: 20 # Privacy settings privacy: disqus: disable: true googleAnalytics: disable: true ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/layouts/����������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023134�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/layouts/partials/�������������������������������������0000775�0000000�0000000�00000000000�15202323100�0024753�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/layouts/partials/.gitkeep�����������������������������0000664�0000000�0000000�00000000105�15202323100�0026400�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Custom partials directory # Override theme partials here if needed �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/layouts/partials/bodys/�������������������������������0000775�0000000�0000000�00000000000�15202323100�0026073�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/layouts/partials/bodys/single.html��������������������0000664�0000000�0000000�00000000317�15202323100�0030243�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{{- .Store.Set "relearnIsNested" false }} {{- if gt .ReadingTime 1 }} <div class="reading-time"> 📖 {{ .ReadingTime }} min read (~ {{ .FuzzyWordCount }} words). </div> {{- end }} {{- .Render "article" }} �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/layouts/partials/content-footer.html������������������0000664�0000000�0000000�00000002712�15202323100�0030611�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{{- $LastModifierDisplayName := "" }} {{- $LastModifierEmail := "" }} {{- $Date := "" }} {{- $dateFormat := site.Params.dateFormat | default ":date_medium" }} {{- with .GitInfo }} {{- with and (not site.Params.hideAuthorName) .AuthorName }} {{- $LastModifierDisplayName = . }} {{- end }} {{- with and (not site.Params.hideAuthorEmail) .AuthorEmail }} {{- $LastModifierEmail = . }} {{- end }} {{- with and (not site.Params.hideAuthorDate) .AuthorDate }} {{- $Date = . | time.Format $dateFormat }} {{- end }} {{- else }} {{- with and (not site.Params.hideAuthorName) .Params.LastModifierDisplayName }} {{- $LastModifierDisplayName = . }} {{- end }} {{- with and (not site.Params.hideAuthorEmail) .Params.LastModifierEmail }} {{- $LastModifierEmail = . }} {{- end }} {{- with and (not site.Params.hideAuthorDate) .Date }} {{- $Date = . | time.Format $dateFormat }} {{- end }} {{- end }} {{- if $LastModifierDisplayName }} Last edited by: <i class='fa-fw fas fa-user'></i> {{ with $LastModifierEmail }}<a href="mailto:{{ . }}">{{ end }}{{ $LastModifierDisplayName }}{{ with $LastModifierEmail }}</a>{{ end }} {{- end }} {{- with $Date }} <i class='fa-fw fas fa-calendar'></i> {{ . }} {{- end }} <br/><small>Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.</small> {{- partial "term-list.html" (dict "page" . "taxonomy" "categories" "icon" "layer-group" ) }} ������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/layouts/partials/custom-header.html�������������������0000664�0000000�0000000�00000001750�15202323100�0030404�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<style> :root { --INTERNAL-MAIN-MAX-width: var(--MENU-MAX-width, var(--MAIN-WIDTH-MAX, 125.0rem)); } .inline-badge > a,img { /* This allows a markdown badge to remain left-aligned */ margin: 10px !important; } .card-container > li:only-child { /* When a card container has a single card (e.g. one code example per tab), span the full grid width instead of being squished to 1/3. */ grid-column: 1 / -1; } .card-container:has(> li:nth-child(2)):not(:has(> li:nth-child(3))) { /* Auto-adapt to 2 columns when a container has exactly 2 cards. */ grid-template-columns: repeat(2, 1fr); } .card-container .card { /* Relearn's theme.css caps cards at max-height: 600px with overflow: hidden, which truncates long testable examples. Let them grow vertically and scroll when needed. */ max-height: none; overflow-y: auto; } .card-container .card pre { /* Ensure horizontal scrolling on long lines inside code blocks. */ overflow-x: auto; white-space: pre; } </style> ������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/layouts/shortcodes/�����������������������������������0000775�0000000�0000000�00000000000�15202323100�0025311�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/layouts/shortcodes/.gitkeep���������������������������0000664�0000000�0000000�00000000100�15202323100�0026731�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Custom shortcodes directory # Add custom Hugo shortcodes here ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/layouts/shortcodes/code.html��������������������������0000664�0000000�0000000�00000012215�15202323100�0027112�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{{- /* code: include a source file from the examples mount with syntax highlighting, followed by a "Full source" link back to the file on GitHub. Parameters: file (required) Path relative to docs/examples, e.g. "customcodec/uint32.go". lang Chroma lexer name. Defaults to "text". options Chroma options passed through, e.g. "linenos=table,hl_lines=3-5". lines "N-M" (1-based, inclusive) to show only a slice of the file. When set, the GitHub source link gets a matching #L{N}-L{M} anchor. Mutually exclusive with `region`. region Name of a named region delimited by `// snippet:NAME` and `// endsnippet:NAME` markers in the file. Marker lines are stripped; captured lines are de-indented by the leading whitespace of the first non-blank captured line. Mutually exclusive with `lines`. nolink Set to "true" to suppress the "Full source" footnote. */ -}} {{- $file := .Get "file" -}} {{- if not $file -}} {{- errorf "code shortcode at %s: missing required parameter %q" .Position "file" -}} {{- end -}} {{- $lang := .Get "lang" | default "text" -}} {{- $opts := .Get "options" | default "" -}} {{- $lines := .Get "lines" -}} {{- $region := .Get "region" -}} {{- $nolink := eq (.Get "nolink") "true" -}} {{- if and $lines $region -}} {{- errorf "code shortcode at %s: %q and %q are mutually exclusive" .Position "lines" "region" -}} {{- end -}} {{- $resourcePath := printf "examples/%s" $file -}} {{- $resource := resources.Get $resourcePath -}} {{- if not $resource -}} {{- errorf "code shortcode at %s: file %q not found under assets/examples" .Position $file -}} {{- else -}} {{- $content := $resource.Content -}} {{- $anchor := "" -}} {{- with $lines -}} {{- $parts := split . "-" -}} {{- $startStr := index $parts 0 -}} {{- $endStr := cond (gt (len $parts) 1) (index $parts 1) $startStr -}} {{- $start := int $startStr -}} {{- $end := int $endStr -}} {{- $all := split $content "\n" -}} {{- $count := add (sub $end $start) 1 -}} {{- $content = delimit (first $count (after (sub $start 1) $all)) "\n" -}} {{- $anchor = printf "#L%s-L%s" $startStr $endStr -}} {{- end -}} {{- with $region -}} {{- $startMarker := printf "// snippet:%s" . -}} {{- $endMarker := printf "// endsnippet:%s" . -}} {{- $all := split $content "\n" -}} {{- $captured := slice -}} {{- $inRegion := false -}} {{- $startLine := 0 -}} {{- $endLine := 0 -}} {{- range $i, $line := $all -}} {{- if and $inRegion (strings.Contains $line $endMarker) -}} {{- $inRegion = false -}} {{- $endLine = add $i 1 -}} {{- else if and (not $inRegion) (strings.Contains $line $startMarker) -}} {{- $inRegion = true -}} {{- $startLine = add $i 2 -}} {{- else if $inRegion -}} {{- $captured = $captured | append $line -}} {{- end -}} {{- end -}} {{- if eq (len $captured) 0 -}} {{- errorf "code shortcode at %s: region %q not found in %q" $.Position . $file -}} {{- end -}} {{- /* dedent: leading whitespace of first non-blank captured line */ -}} {{- $prefix := "" -}} {{- $found := false -}} {{- range $captured -}} {{- if and (not $found) (ne (trim . " \t") "") -}} {{- $stripped := strings.TrimLeft " \t" . -}} {{- $prefix = substr . 0 (sub (len .) (len $stripped)) -}} {{- $found = true -}} {{- end -}} {{- end -}} {{- $dedented := slice -}} {{- range $captured -}} {{- $dedented = $dedented | append (strings.TrimPrefix $prefix .) -}} {{- end -}} {{- /* drop a leading blank line (common when a blank line separates the snippet marker from a godoc-eligible symbol) */ -}} {{- if and (gt (len $dedented) 0) (eq (trim (index $dedented 0) " \t") "") -}} {{- $dedented = after 1 $dedented -}} {{- end -}} {{- /* drop a trailing blank line (common when endsnippet sits on its own line) */ -}} {{- $n := len $dedented -}} {{- if and (gt $n 0) (eq (trim (index $dedented (sub $n 1)) " \t") "") -}} {{- $dedented = first (sub $n 1) $dedented -}} {{- end -}} {{- $content = delimit $dedented "\n" -}} {{- $anchor = printf "#L%d-L%d" $startLine $endLine -}} {{- end -}} {{- /* strip trailing //nolint:... directives so suppressions in source don't leak into rendered snippets */ -}} {{- $lineSet := split $content "\n" -}} {{- $cleaned := slice -}} {{- range $lineSet -}} {{- $line := . -}} {{- $stripped := replaceRE `\s*//\s*nolint:.*$` "" $line -}} {{- if or (ne $stripped "") (eq (trim $line " \t") "") -}} {{- $cleaned = $cleaned | append $stripped -}} {{- end -}} {{- end -}} {{- $content = delimit $cleaned "\n" -}} {{- highlight $content $lang $opts }} {{- if not $nolink }} {{- $repo := site.Params.sourceRepository | default "https://github.com/go-openapi/runtime" -}} {{- $sourceURL := printf "%s/blob/master/docs/examples/%s%s" $repo $file $anchor }} <p class="code-source"><em>Full source: <a href="{{ $sourceURL }}" target="_blank" rel="noopener"><code>docs/examples/{{ $file }}</code></a></em></p> {{- end }} {{- end -}} �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/runtime.yaml.template���������������������������������0000664�0000000�0000000�00000000702�15202323100�0025614�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Dynamic configuration generated at build time # This file provides version information extracted from the repository params: runtime: # Go version requirement (from go.mod) goVersion: '{{ GO_VERSION }}' # Latest release tag (from git tags) latestRelease: '{{ LATEST_RELEASE }}' # Version message for the documentation set versionMessage: '{{ VERSION_MESSAGE }}' # Build timestamp buildTime: '{{ BUILD_TIME }}' ��������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/themes/�����������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0022721�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/themes/.gitignore�������������������������������������0000664�0000000�0000000�00000000015�15202323100�0024705�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������hugo-relearn �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/themes/runtime-assets/��������������������������������0000775�0000000�0000000�00000000000�15202323100�0025704�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/themes/runtime-assets/colorized.png�������������������0000664�0000000�0000000�00000166665�15202323100�0030430�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR�������?���PLTE������������۶mmmIII$$$������m��I��$���������m��I��$����mm�II�$$���������m��I��$�۶��m�mI�I$�$����mm�II�$$۶mmmIII$$ےmmIIm$$mmII$$mmII$$II$$$$۶۶mmImI$I$ےmmII$m$mmII$$mmII$$II$$$$ےmmIIm$$ImmII$$mmmII$$mmII$$II$$$$۶mmmIII$ےmImm$mI$mI$I$$۶۶mmImI$Iے۶mIm$mm۶I$mI۶$I$$ےmImm$IImI$mmmI$mI$I$$۶۶mmImI$I$�ےmmII$m$�ےmImm$II�$mIm$Im�$۶ےmmII$m$�ImmII$$�mmIm$Im�$ImIm$I�$mے۶mIm$mI�I$mےIm$I�m$ےmmIIm$$I�mmII$$m��ے�m�I����m�I���m�I������*�� �IDATxw|MϽ7KLik,vR[mEmZBCUKkծ$BB"BAƽɹqS~=>sΓ󜣪j]ƀB!B!x娥 B!B!^M21$B!B񊒉!!B!BW#CJCXJp?ǾYHeOQ3ԯ<65%]ICWMс+8p$-CGUf=Ѥ\O?vu3hOÛ=1'9xyKSyƭ!N]kE?OcNmXw/!V^8B!B!Gx16,TuQ <:B\VHU MrЊj=ZSQΕoJߠ{_y/[bs9Ɍ"UޥAustw}I,^?~`Ӥ:UYqե:0xr愢˵"~ޕy3@fҵ<~i$q B!B<%6L{5Vh dy)/d[6 [e\&8nHK:#O#*[KCO! %Zmȭ?:¾߳xyM$�Ns:0뀟9j-q'8%>;>s3&KB!B! )1¦R\ c |P&/*[<K!01 XՁK`N]�R}a۠?9zI<:Nmi`}9UwyE5b;-L^?Yub?1gB@F7W}04:NUkJZ|}wRC/WEaK޺@EiLUxpSa_Vo`[Ib߅~*߂3eq?΅ֳ%,�mGc)-bh9hMmiW0z*OB!B=ӂsP¼wلL\ 쾻$ɦs 'Ocؾ+\ϲĮXi޴ 'A2 ґvJ~Rڅgdݝmy o6\!4z߁-olʷSZ_Eԏ&IsD-Vdf棾Jߺ/>gR(mߧ'gZ7U'ޝ atq /qlCw\JSl,lNR߲-{ҔŁ0b;k3mE Q;Aѓq5'qu{Yoͻ[*Z#B!BkbHN癃(~}z,"4)<=^ghpXl͗8C_MTmYX"/ټK qb> `H<>5>\7�p;?{>s+B1Ǫ3?XN\|09dF%EQVbyGG5$>GʖŬr5}+O{aZ y݅%|<ei.~L輜U!^4VCvDKũwb� qxM:- Xq5 C慄B!B<k^̢rKൊn>1s8֮L;D@.9jfzB[iJ]3IsW!#{bѨ lOK(ܯYґx15svnmP cfc}'+}c:Q?=-ҽ W<|3&zx8(Vn3۷3$lޟ88=RYA[DÎ@{zY}B!B<srt^mE~'!'[qvo_\ͨzg-g%o<Z<WƜ-ޣ}wυqŦ-otJ3ϜNK&/6kISc|g HKҒqѩ!3g' %9{Y $nZƶ-ޥ&tEqBϕ$2*؛&zc80q1e+zzb3{փ6k1:Uζ aۊnӖڶڍwNҎݜL#B!R2CR")|FaǼV"2Y3֟5]ز#&UU7 SztzPkԏ0+Oݦna9.UX7O%YJvW<k, g2bH`N/*VP[X',1yk\`9xINFk&xq`2*'vkxN`Zh uDZ/'ӹ+vwm0{B!B<?1q+z7r'uN^'pnb&Mh` >n=w}*d=vnE2uڞCY}^*,MM g<w}G}=Byy)ۙ�}=74cP97[RĮLG&9"_mQs0rri5vmIky;L@`У7(75a]w_nB!B|çul6d*\ūi,q{έ+Y�jv7[RkΚЇH4[lNF:E}KúI `P<t}%nC~Oq}[}0`C-s+붞$-?aΜ~;icf{jm\ޤgix>[WjT~c/+OT6y6&xw1,їv+M7sNmRO"]Ɔin/cʳr.CNf׌j3q`G<@L03̰q/> c> !B!x'070;KQ; X"md\n�Teqz_^ Gk ID `MYzW_<iSZ:ϔ#8fw:ӏ}'R%+ܯ)r_\ Ϙ:4V\?ˡ1o^HR_1ì)A=R~Q )ӶЭBvʨVK ɂ4yk+\i G2 h| V&=ӇId*;# IFf4Mdxl-M!_{uy?Od_!$!B!xNZ1H3gE]7^'ޥ!f,DB$qB!B!fB�#*FaG=%ͶˤB!BL (+0zW䲑{eq㪨K4퓩ks裻X~6CeB!B,% K`eYIWJ'8 !B!OK&B!B!^Qji!B!BWL !B!BdbH!B!%CB!B!'O YCF.*i)!B!B'ToVe'lݫPHeOQ3ԯWa&R#r4-ԃo`(e66ӿFFB!B3{?63ԴT-uOlB*״ O~xƵ̜Z཯cҨ:QB!B!^�fO'6zBNhh8O *}%_FB!B3xusgY yl(y=ݟLs>L͞Sׂ(ɱMSg;NrD{CYlJ=4/| ˾񽰋^(aj6szhKmIT160|z_=M@k7L69QT>cLJ!b_7MuY*1xc[;tSIϥtYִi<<+B!Bهor><5g M~,XZ ¦mj|fv=ڏR o-G@Kq fOHDzt-:M9mwW%ٴAi wY+͛$荭Z&AS:n^)/[J q,Y_`< /<zwME8S_eT5׆X.姏fC ւ c{ΈGk*iyіxũ3/ņԈrm9?~ !B!`y)e@ hSz,5:N텛7tl 1zNb޳/ޟP0$C뀀 ㋱iㇴx{teqH8̆|9y/T>+MTmYX"/ߝ^w:D c�WO9>7|ƝNSS4`&ݻ /˔Zsm|TKl yb}1X_"[!B!B!"߯S9ƣckvvUoAuefN/[{gV]sͺ۬rKൊ~nHp ]Bϴ|x'-De?qX@CgtQтN 2v95/bņ~ܯB!B!rt^Fb͇Fg'rv[B&#Ռ)8|r^sN-Wߩt{CLnnٙgW>uWU׉ztWΜ5a(}F\!~ۚB!B! ׷=Ԩ3`HNAkl8CRZʈgXLtbˎ0VU+| c'2 IR݇3V״UF*7 ?v.EJPEnB!B!^\CbG73$\MnpbhĂ0_K͋aZҪA>.S&s+q+6r'54+Dj|ʗ~S_F. dT!juns.3LIJ.MIHOƐϮ~B!BazfD:E}b_qi=~�'!Me_<zO^A\fIqBS/~5#B1n^HMcX1""&zkW*vmC9Y*qPIj-6'笯. W,]Sx5 =nor%+?c] ^?o<|i5 O/7~_J;v9t~>fGǎ^" ;460iõlg]8XДLe}ҔJq+6z+B!B"DŽ3;"=CSb|u(Ya.-i`Pk ؋au331t SDYp֠D�';y#|xOshdP} a`pRNC6c~/WQ{ ZrY5C:7_tF$gR!k폼Rec>EH k&{4uV>`ebύI?B!B!+e /z_aKU~/B!B!g&MSaWJ\'j 7p,Wb[\I!!B!Bϋ51dVAӿRnsL<Fp8VlLy\ %1pF%B!B?/R2 K` d%]%*!]zW!B!"!!B!BWZ@!B!$CB!B!(B!B!xEĐ/hľHḁ%L۬(lx^TeĽ$!wN xXɞ?R5w;G׼~B@Au0~I'wa'=owT9gn64=vV74ryI2Sǩ|ͽ(yƹO3_~3S_̪0Xiw>ɫ~TɓWBlPWR胃O&t=4U)O/d&1Oա!6GM NUꗢR[.wcs$hWQ?M=vV oVe'lݫP5QSk &ļh8U5zuƹҸWo GTD~׷ K؊ZХZ_|C/G}_\pl6 !lv:Yji"q_*wel?3t KTK}_ ~nk<ipp~:0d%LzЬ5'I롲V:XNaܝh w~|dz(!a3?AM ?KSm{bJ/:q8=py2Ird@SXKEA=$O>o /,96LrRe.!Z%CLA䥌Zbôw[c6@VJ ‘<|i܊z=Z 6*n\AٗOGb.eI\Rַ�ĽQð?`yÇgSC8;/u+:~!&KɌT5U's!D`t Cs>L͞Sׂ(~Y}]h:o=[‚ Іp40B*6ʫ*zvib*ƶqζ Jmr=ۯ& Ə&AeWcB=lݱ;ۦ5eQ?&#~V{<!Rm蝻=7`uvo.=aN]�RU}K\>#nqbDMM>^i46ww(_~ꄻ&| ˾񽰋^(#Ư4^44=>M !(BYn ޭgN7_F{X6O޿5 }r/8W ;%{$|ڭ #',GbrHܧM=Lo?S}Jv̂}uGSN~ݍ%RCSg;NrD{CYlJ=w+EVkJƇ|((ܯ<lCq`Ekls~aZL<~Knq, )WƜ'*<^*>*_iS:~ǯ<|ߞ9ϓF8N+Qohg`Ǭr_4c!"X&0j$W~b.HeٞL#SaS 6@u>3@Ǎs{)7#tYנL[CtoP$&mF\IŚ1iǏ=?8uC!Kp>{ͪ`< /<zwME8CM-U-x݋Sg^{ ףVO!1ސvgxCt:+n%Ǖʾ,kY7^ _Qlƶ8[fҢ˜n:c#zd4#_SD}s,�C&׳ߪ0O] xDƺnF?lZ|? x4,K슕MpƏ#CYT5׆X.姏fC ւ c{γ]s6YTǂe-]x8q0p+6gS#/]0;;%μ Ϳ%p,>KY;~ߌ9~(U}*h»NA5T(f:W7U:.]j4k0sN|B:kiYo48|ؼPO5z瑟s`W4wo@5y Uv]K¼s@ċiWʏoJχ*Nq*jS_y\8W=s'sVv=cS_ c~NT�� �IDATD((nZ|݇ҁEn21쌏֫i-.,q.3w \?î=',c;kt 7ob�G <#1`5g_vfuh/Q2މ4nY-u'MM=Lj;#%;l.иy ʟ_@�8žzJϡww%[蹶f>KLZ l(?Hxl_=84w6iM2v^8t-B/q<_R>'O%dm0C,D`l]h $6Kԥp2JEN):]k@Mb  .p rTƑ|w:MNрtツȻ9L}]ʶa1\mYX"/ǑFҝ[N<I>p.CީX4xLoF?qgH 辠;{4ט{x:njijy_M?ۋ838|FĽqy-]eqW~\ܖu8{ p7챱[?x1Y~јxi|UP 㛱K%CJW~ngt}lktkr|I` g,+wЅ-zE~гY{n ZcE4Gu趜K fsm<8f'Tl72Mtm2}6vF?6Sx͝k #|rKൊdHp ]B_bɧj1/򼐪0c~h'ӄJQm+=zs3^U>#?w),ʹ6 {hhՅ;gѿ[Mܬq LJ\=xGO[z&p>tu~M)6};zߌ.J%Vw?z>zu)g~t4awQX*?fR�}щkauڽq=,#//&?^h/Oy7%tʶ{8-SȧGѳC{,t3j.ɏK(5^y3j |e ț1ƕ çUث_BIB'ܝa~ze;\e>3gs NJ=rMfwŊ |hOf)p[e_fM96~/I ; 7op , 8ԸO=n][ثnr!.MmyCq5]ÈO<k9"`:k FP*J˧.⊣:Wo> ?*ߓ ;ث!Jov682u{P KG7e- TgQ&Z挽>e7ܾ-h4FgPV(&4PVϨw}7x$Ƶ깋/MbiOǛU_%=Oog<YP<I2tcە rVμV21B3$5jk>-qr$/ʉb=p^EBKَ.ZZ('ޚmHJ$E>,<,v+%}e f-cv9;_Pp˻7ɧzw),!)}!]mPd=zdDf?k&PeGzMWo>qF^PZ>ÍTnbMa3@s z Q`=:=5jP̌ F)FNE!<4xߌ->z3{?{}v-?\ +=(,q4>>mňqJX_NsGW{$`2.^r/Oe<@U.(SGٿJvw~j4}.dn]P@</kk8{?XYaV/|emHSfyP6)M&?zVeufnbA%/|JJ!}&ş_C?~ǭȝ0\x}d]'7z=\P7quc0oG'Cq}ϐcPO'- ?8y1:[KZ52KPZ>}d0' QsSkr%5ChعzNGԅX .ݖ[r&|4xߌ.x.ԑ3xriHD>y$3:<G%bĸTT_7Λ?JAhגw?{%bJG'#*|k= 4xV0տJOخ Ԕ<# 8~<W#[`WA9XZO9S2-$N@J<@n)f_CV*Izw+6'̞ˏX6c?^jFxB @L03̰q/0ӾeY|וZ Vg]8'˖ul6d*\ūi,q{έ+Y9GԽ&%[/[T iXCا\~0_&˧g׿/eSQzRBUR5E;e`p{؛]ص gM*J˗yk^n> gw*NEsܹZi&{T7)˖}rQ4bsp2*)v$#suF¨,# ۲pR?Z>S4u"8Tj@/?f*F,<_JǑ#ϬJOбuoeh:j8oeܢw<nZCq_Ke\[_BP1L`_gs<=qEy~}i%O8y pI˶FWWaS9vQܿJp;Sǩ37<# 8~o%q5,.Mm=Y)1D^~BKU+ g7qA 2Mk+\i GL> A1tb/Ʈhli+LyQ^('|~;[,njSH<}ܦF1m?֓(m#B)-{cﴋg|</E4kc8rz[C&jEʅʋ8+¾yKj3)_:Syg7q賠nfN!9^*̲8ߏ?/$OϏ,=a|*J˧'fg e| Jކǽ$=CSb|u(Ya.-_x<ܧ9t2'=&)Gg\#fRW[|ӛFIZB Q~9}{SG}>flǷ`9H<!gS[s#OFO~VB}bǞ3#o8|Ľ1y-]/FKEܘ_cqD JYŋ}%ÖQr@fmft(g@|/ ,_UcYYTֿJt;Sǩ7SYɼ# 8m+%_$\ZyGP:Zp♎B'>Xx$R)yٰ*ۍb7cQm~].JekUgl Gv�9/cHg}҈f ȽRSO]4*q)H7ñ\m:LosYNZīLJH eQpueI]#'q%"^(xiĐ�آ%5 +Caqؘ.cF)' 3J?c\ȓ~N^3-uf|}2umn}tKfcNOJ啻_)f]&ù (ifaYJ&B!B9 K`E d%]%*!]KL !B!BB!B!&B!B!xEĐB!B!+J&BaZ*sl0BW5(lBPi!t[dOt)~1gVaBN I^EO\n}<"̌wۉ^Լ|'r/q/,37bctBUB]QCClx(_؊ZХZQl훼U [*u}iZ~f2q_IoQ9E1g9=zԈ7Ywz _{3 5I*yʸ,QIݢ浖U1T1{l*C8m4U%<[׻gCL:_vB9?;)g9xq+?tܕ%L@!0[t/JIrd@SXK!0$lf7iwgu/Y཯cҨ:Q<ȟSFCx&5I줥s$\ZB6>h C7ed' Sჶ=)׹%<l*k!_4j~>\yBZV~KJPUyy6{*6V&w5![w^7fE\N6õj LvtKAKL !i܊z=K[>~ϬJ_~{$mcɗ[ډHɸ\ДafTʞ[BNhh8ρW M~blsj<ugoy: cֿN`,>|^.֔!%U-H2�!1w==62yr?~ {RܗCbBLd)Ba�K˱Â^lC[%{$d[| ˾񽰋^HUa*ɪ9CЍw0S|ubԳnMu'>qg9s+Gu|LJ!b_7MuYMavoĞeoF-ɺ%M㉬Ni@nmR�=qI,4yzf`z*OWj</vأ8mpMnS_HCSg;NrD{CYlJeodszhKm<:ݩ¸Wm蝻=7`uvo.TTj~41~0*rRnByL6YUyāqP>AWۯFn|^ph[x `dgsv}1yL *L*&iv/+1xuA * %e=ıW=CncHO']#w !P޷"x <ii5?\ ׳,+V7mI~-.Ǡ~4I$Ror'34`ʗu횳BJ?,- fgę$'`DT5׆X.姏fC ւ c{v1,z;7[hw)Z�Tk] ]"N-R#RK:Ś1iǏ=?8uC!Kp>_~ɍKq fOHDzt-:M9m�MHyhl)B3` ^�ߊ[q/KZ j3z|4-N䟙2[FsP¼wلL\ 3~-gڊvx'1o3jN q`L=ψ]>Uax^:Gs.eѱ7=49Pծ(w9kS_ c~D<հV<N6nIvm^:GRX_u)悆+>8.]p(L1vkL9 !CB!xmӖxX DKŲ8$foߐsz׏kI_F\)e@ hSKt?8Jd2dzi4;Efҽ "^N](E)m~Nb޳/>GKp(k_cU5De/b}1X_"Ҍ_SǕ-u'MM=Lj;#ߡ?9ϹijypDp|16mo/|Н='DF{@6Kw'[ą^"x ~|)N<J`Xw33QXDh9.S2x{ϼ+iIԡD\[z'аgJacOyu.eqȑTl=![ؗR5ЕۺMLR{[ĝ9Ė~ٖYV ["Oښ,֗2kW)oil>#dXC5xoN<RI<2/$G !OKU1?^G?Œ߭&n֪|Oǐ=nSAmO;zBTV.UtGU$6U7͠LӺxJ%Vw?wh(y?Ù/KC>tQтN 2v95/bņ~2MWoӠ-awS ŭ)=zzjTʵ.k@*.f[VhHp ]BF em>l ۷=N꧈'8Jʧkp|wq3.ET%s|ٺ+Ⱥ Z1)TtO~\zƷ>K>WS}s7ۭ6|i?OȚY\EJ|CmũY}^Fho^iB&B;>͚sl;|Fy_N/fTOY|ā泖7szPW$]GG˧.⊣:7sI舽:ĸK'!F N8dۯOX{;})󟼛ZCvYK}ן_~_338s3MJW{?<5i\qp0"ڤߤ-nF?~'N3$brs 4V~jzeيX} GN#V *u|{MQd )s¸՟Oacly|βB5= $45~6�7/ypGZֽMۭ"\<ݜI\9ȿg0͏͐KxR2!)O횵|2`c|I?kf [vׄ1xF_g(e/jyΨ?/YTꕗp#XS rY`HNAklc=$ܯ>z3{?{uHfŊ}q9I3?êsV7PD_ޫT 5lodqעժxDģ3etF##*Nj;aƽ6wo;3CR")|FaǼVCϣTN+񌝰6-Aן?]R!o\n$RpvAE.c <("X;тF9_ qL,3_#'L󠰾oJ!6<0vTL;Ȫ}7sMx0gb,[jR()CB!S]n`NuD]UycMOL0n-iʗz*i*GTylmi%}JyOɨB\eH ZΠbhĂ0_KOns)8ioP_F9wV0懓FHU4%] %>9Űk8A_-=?eYƖɗGy McVZBy)[{щx[Q;a\'R9G OuwJoL*'èqatri�}:r&}"WͦodYAڂuOO-0˿GY<<87\�� �IDATzǾH&x-}boQMGlDs6!)CB!\86Yc AcLF9:n CЏ*lVI N)y,+sSDDoJŮm(G8kB3 |'aNs.y$~ ϓd8K^q8Ѵ?s&S|ٲv* iZDpԀ._~J͘UXx>_,_qV~KkixzP2ݩܶ;ͳ ' x.g8Vϩ^1îDqThJadiJEGԽ&%[9.Hix_װ}pKfr 1Iw꽏Ʊν*͆ =Lp-G=_SU*}f+/8 qN-{cāGt*GJ4[lNF:E}c.W>Ά+`L\7܊eD@L03̰q/p~ɓ) p6|+-_6e^.%(Z5!%DbB .1cR&B/p'JjXCC6ɷdyƵs^ߧ4)/*HEq+>BeHm4~ vOriOEs=r~TQ?!L !/״[R[̝IO<QGͤ7:sKh̲8ߏ?/$OϏ,=+w0K-䌁bPbہ.|X<d1%f̷[ơ^ G}>flǷ`9c Y5C:7_tF$gR!k A1tb/Ʈhli+#kt]ϫ=8]q=А|G2߿)+G.�KU| ޿gᆪ�C'( ""ػbذꪻvD,QHAzozޓ {//>d373l)^7B~=БH{پ<yTzqeggꡛ91`*)LKfk0ۻW[n#OH_eD?d?ه%=ΔPUʝO^͓ǐnVMxmBR<opc-gx8Ty%g|q/b[sn>y7} ",b<Gs"U1{̤ _t{{ε8n}CGSey{^{؃aa.ޚ`Mپ=l&c $-$D;UfB*Å_)&qQ4ק\'\2dI !^;Bos0XBgx3(}0E/0oKƟ?_!$1$BxwF#E} {ŕ s ;%;X߹4e=W=X/7I{:|fQ^z,Q:(B!=D%%*XqdgB&@CB!B!-!M B!B2IbH!B!ĐB!B!D %!!B!BJCB˯Mf]Ϲ>IšB!BK�Z8xqy'o&"M$ڐ̚yWՠd/2{:.ғʛ"Ӕ!%B!&دcEq%#RV7ME‹8mfr6 SPX"qhKB!B I `R]vi q9<o-mǎo<Ĝ\n!B!8>Q2Oz0O|a ,wfa!eo=s$Z+1Lٶys_t[)]:GQsgsڔ)]!:h"EhQ< ~̊%|x.zؾ'__|O{7hog<Džc�CfwWU㗍v72=)Ϝ_cwq f^NJdVe/c2_h!B!C0r]><{2 (jb#qpx"t裼u'U(;Kǔ/X{Vn$5AONSٴEk#/_Y9oxqF{.F'?ŝ/nYU "<){}/y7x=ffc29NȥadWIpKy8 qd&}E~ #3CM`~ 2&!B!ACD0uW嵮ӹ7<juƃI9UtnՒ/2VzY7{'r2OC״,Mh4y nsyXz0^lyWjݕò$qwctݻlev^:2LKWp3׼uW|BZu*+s,A҅<ֆ7ɰXțZB!B$1t@9?z-o-!{)MfHڱ]#ef&sN#՝ �/}s2֥ҥMc+_WOÐ~1ii �+5.Zβ0~p_I"aCN0Y}LlamqH؞(?;EDS2?B! I <c7)ۏ˧Ut4F99=c#/ze }јiO+cV#/U1%,r~D`&R7½GTmo 2huk|4Dv7_N&̏^yK7 !B!#JQCMAQ\b:H((XB-u-_Ng^Ʀ ŨSES*bxg|rMЅ0Z~|#BUPt U ]39OYnG[zDc`HH!B!&J@t 78 hU2�v/X~#CPCwyip{_uN~'.4w9Kx;v8 \5DKߞ'ML7ߖKJ)WD(B!B&c%6!:оˡ򥝥` 3&~ʨ3H}~zl,~Sw`zY6W?і33MuIzƣ{"RX&Y׉o&sOۗ~|rvf#i~)No'}3yz8?klWNN }vh9;V=,p?w^֓R4BgLxΗ vSh#4dPͽZ<v lۑ8FB!B$1t@8g=~>W{>{~*27SVi/kh.k2{=u.S䢷dcmxq:)&%=ʈlYv%=I )_*wL'cH `+&mw6c!)%{'j`\ ͰCGhL*GPAy^K9<03>X8^{*�'Eo^ɶ}GHV.cŻ׬@*2vkiq!B!8 ?fBݍLpWi!B!hlmB!B!-$B!B!Z(yL!B!;B!B!Z(I !B!BPB!B!h$1$B!BBIbHѴVp!JB!h  @4G#jo"i!DS.ᵴy<6̿~ HXy.^6Waۑj` ?c9;!�XV^e,oԒu5�ch,kIJBP\eUX_?zaNe@?0dNscigO9pk7$:)#0섲1]yp|: Vy]Cw~1~1hmi[CyunSonC!|w#Г(7tY~pZ h:�Y^PU޿at?ʾB"P݃ Ѕ1j̉_߅qZe݃T7c|[xs19e4=u.  E;n7vyjN8O&8n_3R<oy^H\|`scA1)n5tlqPn6q<jzOB4P=/6|OKCJHZ\8Da*Th z.4?jxHN=g%Q:ʺ&ؽ"Q*sVX6'؀R;\g$Q‰`V'\cF_<oy.k#.cU7́uݮdMPŌzӸ+!!h3hB+M2-. NwWlUu]dCGndžjgwOHܐ&q _WW7Yw'5I g۞{k'>&?o.C'pOUnv۫'pX]BΠ{D@E6_gV3z1} tܚHJANL+?)0昪;UUwm;K؏ n}O]Гj"y>b,w ox)ﰩ1`|sS żd!:Ce pIW'@PTr[}7P^qi`_r}p}g;׮PtG ֥a2*�.:3- ! JK1kab"W9cTA8;f阯mDo?1(ԍ=0Έ!`%R*=GSFU}[C.<hg䮨{BV;DCƖ{۹s|>҉CQ5,+ sZtRpA[:Td3`'=P٩Gꃺ-S('UWlE?6=7ǍmuǸ EYL(lWסS5kתuF@[+Sڂ:t4LS 侐9v+i)x|,a^wGO“0~ N_g)1 +�:vSw㐏G=GdzݸV;{lJbH-{}/y7x=ffc297/wx^z.[^IY`on0~ V+E9%f )K!˩\x̃{rL:#6Ô)|*LtmEՋ5^7sG0:)|qUa[c.:W"Lr߼?BE\7\v#q g`PpG0T6miUw6?н G?9qQxMU=:Io0D^kZw/Դ>[1XwnB]x˛6p@/ C]ݼ&dXP={ L+̅Gr2ƛ 5  )DO2C 6q�'Ns0Yty;<5 ,܂~�qG?iaƬRs|,,@?73-[6⺭�!/u۶17Ǖ*(:$< )Gzxgx&:\+KQ. :> #ɦҝ :]`6's1[ƃP,1$qM^ k Ţ/[યvMI:61! !UtiM\0 <Xs6a[ёpnW (x8nw[AތSw'IbHȒ/MmosucLڭ{7R0/yvI|+<f:en'OfkHuְ|>^[;^܆va6YFcYQsCʴe^`P\qgwW*NokIg&n?/2Vr^#!M}9'kWJn,i}o-*U nC`c+zK/V@FY!*^=[0يv�a>11*ǥ|)}V@1hF:n_wZW(̞>kѫ4dT`M+$NͮC=]Q&;B*D AϪŝvvE]UhΆ@,WA5$jC8�bT$BVUަ< /KAO-K$`Ȅ)V˼t{8nX17ɯN C&q@Nz>硭X^G_zV_qRݫ {|&‚t;cJ\APrpc~~ΰzs/` =;/w'5N}~[nƳA;]jn@h@*4S?ggtrn\#(0.b;1 3~?kdflfγhcxPn y^ ܽV_;SN3WfRzO<7r0!ÇcP}]'JsЛ-pVoc,K0@Cdu=cPg۳N( \~t${ПV~!߇j7xT/!g>9.X]c[CL l\K! (ѿNX?xj7bP=5zfj^5B= տ'199o J/BPmDBFYC* 9gk ԭqr?|1ݍnul#cSok rǐ &b;KX>3c0@p/g%d>؋ R2nA~v1KkDM<FG*؞S޲ hsQ`"цX&~}cD)ԨS1E=€.LLdY=ܖ85*g;QaoW`'U3*"e8οdVt:hwkҽquηC _׵ezrGk܈ F+d;%I\k<q� jmq2m T-PIѝ eWEzC#"�MX+OƩ/wmqx<dݯK;\/+z$hytA>E"揻wُɭqi+<<V6[m% kS\6\7>l6q`X#^[.( &6!EQHiBOAM0\Uˆ~wu5e*RV45tܴCT}QJLXƷ>m=^mPqAaĎ~ԜGTl�۪IpޏoA+Fjh0Όżqa<e^6TܨpTE(kO\Uuj㯶DZkqY{$)Чq~<:)֕=~6释Cco'S_Txv;ROٮȣdB4d]ΒADݤl?goG-m[λ<T޸tIOXk[ 㼳Bػh9Lʵo$]gq]BVG<&w+Xt9In奉Ih̆VAM'k?J梷ZPRK!OW�� �IDAT>j(@/.qhe @s>8^f4UF?to:Ղ^Suv^gaYz1Ƶ>>]9qu4ѭPЫs8W'c޷ 5ټl/\k|śv)U D.خ>dL"1k˅:F,  btq=ji?uI':@뇻gb<:zŝxv D!Rv\|e?>b9;3с [?gTMeϞFH^~z|ŏ 9?l?Vv,s/L`;3X.bW�Y kkyi2 }Vf+Du">>?/)@[ʷOψ`Bvk;D}}Akhhh_(Qһupo/?0TQ&z \v>z qV=}m9h1.R@:$)?ۃL ąAa&zccU}DDn*؅egyuqA(ԅQ_jݻ+?na~^qǫO@u)E(<':8uAeGZeC g> ,1)=.u? &qqͣv*DCq]-N*Mf[K`t|ꛘ֥ $WcY6ay'}U'imPux3>>^?-Wqvnx$hL4K^宑<y l٤f,$DctªqI7/?oeS^Y9z-<v,<9ڣr̽? V=t38#'L%i,s |iO1)9xUFcJf+�A+sӸkhGeA{g]g&&S<}ڞdǻcL% (jfۓQƈ@^܃k7(Cnj@%~݇.g,eqIwRzG.:Iľ4Qc.;@U]@5X=5q[qoWf);%^lq洔g3++b邺"D/߅uC*HA -fdlǕ1tx5*[%I\k<m[QxרzyM ն]I^ΉFef3~VT1g!zF"tx_[m܌yKήQa؊^Mdqr?|5!zy!]ńh68y7\uH*wD�KQ?uX <gI'rΩ4hDqC�CQG/8qjdy̜X iZ5kB!N<dBq䋢׀ÿ!T&kJQǢ( VG 0xdLgלXb0>8,٘ɺ9n:ţZ3]K\&qM:#'܍$!!8#o(uf$7P^1ƋH5%{ѓQcOPPg5s1n 6c1q53B~BM+ḱ.m0nknaUm-ԔƵ5k-4 Yg`]u/zwV(B!B!D$_W/B!BBIbH!B!ĐB!B!D %!!B!BJCB!D / !h6a "PZȴXᅱh,@PcD@Qg~m2wT~IM7(^KӇ;>/K>gŒCK0Ӌ_Geh`Sl } z1>̹;ӆo-:푽Ype_\Ճ֡ =X|u?7_ӞK TؖYe~ mvj:Obi�Y5kUyCN$>pb7f / j+;su~6ֱ4/]҃</Kd?Nqk'.Sn^qdCO2c~2.Ϲ^v?(ƛ:7FtLUsU[9o5(wy W`)t/bl͹<šع=-<mjMvݴHp tMSnj΃uضe#*, ; chMSS_Z4_@Pg _S<oy(&qqM2|xǿO v 'mܭk)E56q<ʧp7zw%1$qqچSQBQC7F8.ʎQӅz8.N,to㼼 hJ@v&/m-_6޵с c%ysbI pXVр=.]&V hi7kyPqM$5fRTf8]wF5_ϾӸ+!!h K+,AykS?4.wgT~nv'b 械EI#J6㓸!'qMZs~ǧxzO=ڮHbH?קŝI ƨ,$m/|2fm*f2z1} tܚHJANL+?)0昪 /v; `ԥ_ EՅnd{Hr/px|| ɿޘ =RKaScpSDrޜ/o5Ey[]"ΎB!v~UיE6X̸}M}871e7mfNY,ŭC@Bcs_M]lI.�rgSyxܞ\:0"Zm173<GgV1s7h0+˘K} A7<1dwV!na!vv=;>pi͈�B{ Em1xge&sJUؚG5( X+lJ?٫G %%rGN R\&*$S{:/Ǎ* /ӣ Sr2YF;K\V?qä o:ٓ]ȴu,;lnvQpgXΈ'`kf>m*`]�D\8=o'tEk/?_Hm=낻⹳O4b:؞ ZAS{>ٳxnu=C 5N%1$DOL4FTg8eeaL:#6Ô)|*LtmEg/cw�5+730$5AONSٴܣiZmhKT<>31 `Lv4!!g(oɁĨtRjuߠx^H\nSg9O^T?cp8xsm).LM`灋0rp'.)-Uj(9cv;Ҳyi =PE/7<qV{d\>/Q B%^�5Nw@喺Ϭd^?O;ʮ+卭ӻ/Ĺ{YXY;׿.Xr'rm TЫ_8Ɵ ثݯo\ty}I۝>mx+1cCi+Cذ1GjEXp�kKqZ֙1d1eY9Ȩh>!!S*qMZ5|6!\/Nsqڨvm'$XTfQAIP0z⵳yl^q8o!ǖ#=rYrO/!#؟8zXO7%歡18ҲxikPr~|rݍS_[?I<p7Ξ8u+z$h/]Hacm8}Ý ;ُe@MxQp#FcCf R,fTNv| aߗ~}Ŭ஦N URV24@9*bX4aGAud%. @.@X.4mGڤ@&0kga *4AF(d䖒qvRsxΈVрmR?3ho>9=)\vˮ+t`pWI?]DhXZGېs$N[YٞZ`@UsfemNy</gUk eKEDYJټJ*rqyv8n�.9!,nYOzu\/DĵLkJA:( gOvurqXqYk>o\MVjsE?y<MI�?�;% 3xt#ğx Ӫ%b [lm43㩻뇻[f<܎zsVK<p;I bamqH؞(?;EDS݈'py1~2{]Cj(g)o]ڬFEbRAX*zm/ge><D06 ڷ $JB h=y,ǵ;ՠ•+$Íf5+ y'9K*3y~Zo9&heMiLOx0%e2U4نfG!?g%5n0Nu0%I\kq"i 4\PNO7%LhN|ͫH2>Z^۪]v2M((yF핔R^FY8v1 69z8nmONCBH{&dk4}"#(Mnzo?-S7'vi #R)N> :G #\A"EbR{®ټl"*2e!2�r+Q n[sNR~\H4\Azw5x?GE&D[PlfT Ư<8+-_\#KOĭN?i #ПhbCExw5kUE_ L|",D*"fE%i.ڇDGEҫ5 gCx�tգ\͎ʦn,o'ԗ뇻Zh<Y-یWIbHHp5u;r՛Ԉz+: ׍~M\&(Ma*|;d&CFF=VUc(uv'Eµ{{KzY, Nt^_āזs+1%vH q\0N2K`k) QZ~\'VYysadzMIjV#\T׵ ؼv{E3֕1c=ZGsMV&gxSvT` qMZ#kZQ HwE6 4Px qBRŰN fî]Ҫ3wv$<DZX+b/y8N}~[n⁧\[^&TOٮ!67TL1cA1ImxeYJqIxxiЫ?}"Tݏf-)\Es|3C*~./MNB#l?b}6)CXGڝ,gy�JKmSYY6l:1,>r[wca;}?(.jH*'K{n>xmLINu Dݠq-=ƠHKPˆfE9+ t$FĵF|.?t-i[9 R$5iGr~+EJv׏0n:#d,1i&eigeJ aЎq�wuaƩ/w˭xu+F<d"w Ѡ+LgLxΗ vSh#4dذœ^`/ zgK֥S <1kavYX&Y׉�IX<XYBz"[Ob Hj‡[0e#؎/Źxkځ;ڲ{bmi\5+*Is~86o-,0~qz.&Vb&?PJDk63bh;9#Rmu a-z pȓ=53l!a(WcvLvF aAVSѰT@Zlp.I߾s}qzPb c8mwڪ_ؐ&*qJ ,A YfwX=|ɪ"C+n$^G(a㛿|{O-i%? i6nKJ\(·<o6?gӶ AVKws;mc.چNў ;S+y|G.I 132>,.H.)mX{^XƄ3g>Y8wx"oCPH( Gzƻzxpzߍg]OYHbHF$q۟ᵧ pRQ\DlgH33X\t㌜C0%5Z}H@M_eD?d?K Ѐ5 o䩯%pPEEl4=>*1h]kV ;X58^1w\d{\4^U·hsr<OidŖr~cMt^ kͽsnVY[:l_h>10(?${Ck/T+nچzjI~Q9;5O44Κqy}FSTaeer?T}WCԣ6>$ekx`K!sMJ~R郢hUᠠ^Ͻ<iwcbyK�!^b/n*{eIfa9,Be Mvj:cuw8$(N-qMZqhu>mp[+%׳sx` lfܦ|:7y%ӭ^I_d% bNzTƋ_E~6ηsSXC # uy)q]g|?N}~[nƳg,URiBQ30DŭطhuﵢoL㚅 J\+>3SS5l`8&}Ů9 nfz~cUؿoqZ?Y|7/lXgGTUĵׄ3BxÝxv HW !đ/2,W] 9].*;'75)Jvh4U_`Ca쎡4_bpٝ\Œ>j' ,&3C\dX] :"ʴ5kׄ3Bx2D݌݆$!!8Jxhh/.d<q}3A‘#Th{1o ~?on;Gzƴ^(b}5+Ol# [́":2K{GҊJ6ddrǵ5k-4 YgӸ۠g (B!B!D$7 !B!BPB!B!h$1$B!BBIbH!B!ĐBX,p�� �IDAT!B!D %!!B!BJCB!B!-$B!B!Z(I !B!BPB!B!h$1$B!BBIbH!B!ĐB!B!D %!!B!BJCB!B!-$B!B!Z(I !B!BPB!B!h$1$B!BBIbH!B!ĐB!B!D %!!B!BJCB!B!-$B!B!Z(I !B!BPB!B!h$1$B!BBIbH$DEgx:*~w?ԵXR߷_O.B!BIрOYtQƶOe]a.CX`?%,sen7م/KK۾^l'B!9RCji!Hp5nᅙΠpHVO_;ogo)X:#?ϳ[uB!B,CB48MyNvn.@o̺_fb]eNmT)ʶgQ}B!BfI1$Dce;x�W _?_̺U?s}ґ?MYybh q}S+x]=Ômk7^H/ !B!Ds w Y׳jѧ#Zm!ϹOF KyGEko廟V=d<n?F0]8#i*B!Đ}=h 9MD<c_ 4$]8]}_NfIc%)$B!́$hT#93g)o]*"B!͈cHFɟ(ϣPnB!BQO$1$Dcؗf3?6M4^!B!u$B4:t^Foap C2K\*~./MND!B!hCB48Ehnt[NhZ#gGxɳ?1;JJcЦh~Ŭc4hs}@m=c1[ɄB!ɓĐ IeCuQɮ%1uC|GvζiNoÙ\ͬs@MreMf/ey$B!9PCji!B!BG"B!BBIbH!BwaVsnw"(60{AEFXc/QEh%F%b,H;첽X" la<<޹|ܳ;_f戈SJ SJ SJ SJ SJ SJ 4M5.x@H(""ä_r>CDDDDDZ%DdL yh+ZӪ>}W46UEDDDD*"kL<6,&N;!s{(1$"uRq-V 6DDo'coQG\[w?/NyL/Ǥ%xp`#C#4{>6N~1Iǩ?+`B|NYpMe} 3W1t_LyI\1z^ļL[?7_9ui=4cHDr.qxp1WG*bWo񛿯ٴ̐u Jɟ~s7!y#8{x^|Xj=:|r=Yʳ*'s/~EWE^M9 38njƑֽ;vI 5|,~^|u~ny|�,z^C{x <z%\Đ"J H$s;9׸rس,ɮYo`px'<S:mf 3}LD3]OOyFE/'u, ,b.f?%WϮ%!=/o|V;Mas 6ȡ)e݊5C[y==8νltf7AzJ ƃl^F?biruVɤ_nPm2+//HF>>3>8n]\†C8Pv;,ꔶd*Sf= TYb]÷6u<oU`?4\WWDDDDDZ/%DZ�>Nj {eѦ-WwQ*7d]m>]&}s?zn`@Qa sK RNK#ʼn尗&3uvmS3HvI!5Ւ>Ϙ:7Jbi^em""""""{C"-#:~_am565_rW0ok, >2:@Q1.آBJ%|u(^dT|pK)-5욅C>nKqu=-*č#C%DDDDDRbH%gŌ/ט<]98>ev(y9T&hNϺ7͟bsj]Ky|Ԑ[NiKR,bv/Ә2 u6={U!uai<|* "R40EI!X y.bK3392m}p|(Fr =ő8>s7ƎaE%`KY"gI}c{ ĺrqhr0<'&z1^!ο|(+( uז|#oN!T%6?NM:1} BDjIt.xA1|y{]ߍn~3c~:ҐW*ǵiؕH&~m8zu!Çc`i.kgKDz-.'`~}e~N]7mP>#q1_=p fSאtH7pdo:x.Ⱥ%DDDDDS8jW/""""""{OJ SZJ&"""""""NiƐH;ĐH;ĐH;ĐH;ĐH;Đ}8k1+|=tHDw-]UOrtbfR8ǹ4G#KYp'qvu=EDDDDD@!XTLgQqzb !3w YիXt%֔""""""ĐEFLaƫW!KQXDDDDDDZ;%DZ ]wlrup?>_9TefIǩ?+`B|NYpM3| 3W1t_L߼O\#_琺inZ{ov[9i;>~_H@+*"s ~|.eb&s{ͫ'pgpj<>32GZ%%ʅYٽz\"�X%{-e{/m,ڋ#o{%R+("""""R%Dd18<<" ;&Mu$?,@"C{q_rOa 6ȡ)e݊5C+a^Ūۻ0t5})Ft EDDDDDjĐ7*c ˅<W@:15|/N 5XGE7DIn ӵZVDDDDDdgكLֹ~gd3+Ior뜻qIH^.So~ƻKדYjp _}'TK<IfW9tisO>^[^=DDDDDDvA!=M#�&G0:h㗫/C넯ͼV/8x}L"n)]pgקpxZ7p( $(m!ΐ &v~ϸMڀe]9﹑(k$"""""Đ-%gqi&*-Xpö3o\9rDw$ͭ_cw&KB~b)#/c_/v}r)-vIE,e`Xt-9Ɇ %oIm�[]mY9&tEDDDD}SbHDjy-Ƌ:Quu%+?78<{=Q?IQ5*9U:>G1=*O'aqh,z[_KY_aH|[ԏy&%Ŀ'8%G_2AuYT| ~9s%7w NDDDDDt?0JV-dO)4cZd.'Og^&rs=> [}yqo/ ObL%?OE[=R:w9Y8Q\t9?3_3i^iͲELiprި9zP ų_&1d/^:x%ٜxBwÇ3{v, F1꺳8.hc EDDDD1XHDDDDDDDҳEDDDDDDD)%DDDDDDDD)%DDDDDDDD)%DDDDDDDD)%DDDDDDDD)%DDDDDDDD)%DDDDDDDD)%DDDDDDDD)%DDDDDDDD)%DZO&)8M[Ixz(#߳GΕY6i% ~d)-H[UD0 s|3_]­/Ift$w[rM!+#zYfqe{7o}U^: <Q< IŜsDmƺ%5is|_פ13x,z}7C^be&1R-XOؠWnSXto_{|!D7ގDDDDDmR2=ρ>4 oo�%ԧ\㋬.;4K0P  6&ֲM1$G?tTG*Ȓ099tB3xUu,W밪VԚo=|hO۰ }ssM܎DDDDDMҌ!=ם{ƕD<=9r |x\oGVˡO|ɔw3=#pԎvDDDDDdϏjDd1 )$,Ux}�_\QU{1 np(J70Or۝y ۣ$~{޿XjMWqڱ[J I22S'2ꕡ/WONy} ;29."˚aPVm`|;1W91e充 r0ÿb3?VG[v;t?K!=VSt9S@n/wG<t 7/*axUtlla~]q(UEx9$8IQ?w򰀭'; ڱ_cHdO -eb@/ '^w bSrUϊ?1M_ r 6Qqx[|;1[)J<*ZJ,_ W_i l\Ŋ5?[PkhǞ"""""niƐȞ]ϗr#8[|d�cs#jD}Hq6Ե O$OJ]OAv)>uޡd&zH`8^]Κxs9!v>m,' N*fȭ-hZp;ZE? W5كg0N~sGlt=gVPRy9R]5:>\9py?>Q]Ks/M rڨO) W~}:gY/>?9 iG STf v~ """""mC"{=_Nاyⱱ^Y/#h~ .Go)rwq xiFu+&{mH(1$|hn7_ MCs{?Rv~ """""C"-L 9W ,OJ&\PLжgJ Sz\H;ĐH;ĐH;ĐH;ĐHe ٧0H#t%$?UDZ#u^0[=poA{GF*`8dϖ͜MeSc1 Iƽl5p8wtBq,}'Zۺ8WwtUk>ŝid˓ ٸ]Q@0ƹ02ԓq)|ܑ`7Ƈd6Uvv}EDDDDC"p ʉ;wZ؛β@d؇} nGӧc'cĂ?a ?҃0KqSڼ1%_gNXl7=] mW  ۆ_-,e x?bmh) L߾8'&asbL[\زfNHQbHݰ~F:VPrc?mx'5ׄ1 e085>1a捝3~Kf~=c qA`#C)v488oso.nS(l(饉alwx19�)53莹`ksSoWWDDDD1_;&o'IfaZ|x`uV'z`68yp�̐ í6/9 ?u\b2YӘi�%DZn#K͑|^0N9+`;u5؜Sρ`1\xcc[owu}0R 쵸.l201ow_rK?TG{αI`}z.vvS1{Ń?~]]vNL^ F_]_=T x<l4#%DZ]0|p}@8%>xQ{llq:gaiSM>7q? r:W| a^폙Xۃq^==kX}qJ>9j ~y67籣1f}yNօܭF6lGlE 3Ueub[ *֋+oG}yqRdS]=Vc?ވHWL*lI u__Z!ibɎgXQ`W+""""�J //6۞#.6Ȯg@w5)7"Mo.R!ڊ pnY�wa;-H"GE?mцY\`Sy79lƹnf! m3#m@;zÜx)eъbl?x츪uf=O] ؋ qkYRgBZn8#׮S5~q}EDDDD@!V@F V|`ڒ,-wo``A=0 a9( (oJ{`x_o[|Xc`d˔#๷+x˱W&ǹCFsR�Db0ne%64~hlm9i3O~;g1ihv4C"koZO}Q,<|_J `8۽q`NLͲF4 d�� �IDAT̈#p;j)nts-?$ߎp?3"~{0#N}3Shwk{64~y#v̷cowH(1$jX ns?Z Sb]pZ% b; 5`̚21pZg~:k4tM<=sCPtvrS,l<t's<96`2 q_^ ~O7m~_ԫ 0Z^_8%~tѮ׾W7 jhv4C"pAL"l~3"5kZȊEA_DCmw~pvj}YVS" 1S$T \mӊ@ WBLmp$;Cb>LEu?YWDDDDiE`ؿ5sJ$0ocSqʲq0Fù8]¦%bJ-> 3i6fQN̘A*~(vj96`nD0vOؿ 8۫ +@f"gc vlN)Dj~[6�sU[E%9ՍhVh1pb|+`m_.R'""""ҜiMrp[gԁ8Miƹ<܇PTkVbbߛUqR<PP}>&10k 5` 9؊h~0g�ι6|ʃe❳J̥}0gs} ? jC{8WgbvN`yف25=?Ou/߿}'""""Ҍ}Hkasq_Q^&΄p'׉Nѫg築M w;|6TK.{y8fDnӶW+""""�B ʔa1߁zbbj+h I~Hx~`Ql[Up HCwۧoxkmt̩YEEڞޥ΋bsvljS~4Evu}EDDDDF?VDXs4bӭ33pNj9Sm5aN0-@%OqgZ`~{I u\YL0vJܛ<m9iDiĿhL#, U�^ ?&d"B(8B""""z)1$"""""""N)1$"""""""N)1$"""""""N)1$z=dQ[dDDDDDdOPbH{?Nދc[P<q\~\?&ڕ#c뤆=퓺sF<qvps{q~bkM4~|2 vۡ cGvZ<J?<?OëGvg'"""":yʐWWrh<áuH^^M5/K'}c UޛՅ6M6mjn '-?iqğągrB88A/`bG6~ֵD,5Z"o:ڋaւl x(t}ii4:,egQʆx5ōTybRCA&6ߣ`c[Oq{qa,\۔qv)Qx.O7*=35!JK7?SфQ@�l%GC&Em&M'""""Ҋǎ H[aH(y [2 hR2N+*_WKylI5]4ml(BQuR 6:LَMC*f/uH+C"m ?,`ak_z}9(O<.0ٹ?l `|~<[Eŷ63mbn[faY9StH8/<[I0*ys>[ſ+mu-'O,-9Lڶ|F,FHghDc Y[\?0#|I)SLaƹRRu@&'t%ݸ/9/u7Ÿ~q]ZBXs*h%Yp+l7iס' MJ2#&fQv!o+bvu+W~J A@9n؜AKrX<sf֐秗7Do4w9/aįz0l=7.b3@t39:Xs6*Jk{lNθޞySks'#=cxl-1<>p͗y,s7Cg?*ncT1q a|f nb> 6]*6=Hf~cjñp^B1q11iiDWe_x#WN,`v6YVƉn5_Wk/9upOFVl5BVӇ:t#30y<}eqؿO/_ODDDDSbH1J[ϒhe2cI V2nE97+fv%0B% <Ds[ʊjcU-ZʪBQNd-*~̮`dNwN<P'~S")sNK1|g(u=T #vwYWOc P)Ņ=bfqq2<F6Җ>~beܲz{"5<~uD-n8̆/eb8c@ rdjѦY=ʏo!W- ֵn"""""Ҏh!879A<zj[o(b'w ġ*V[/jcb WyPzȈ5M:^f,+du+Ɉ=ɳ`YSWete•\6aw,$;+AOihOK:Gk),Sr"""""f �NR&/~#%\>!pZq2nywNqzL<'Wb\XĜ'�Ϧ891>L9Un-2l:usb ׇo7-d-.# &fMIsüpbGV=kƶk,!Ÿ, n,${H1_rGHDDDD%DZ�?NO0w{piOwrPU̮^vN:|xr=To{̨mBXL-ڈKI׉DXM]׆"(Zk;PPjt>+kXEsE^uVV?. lx E)1YX/#ٺiOi VFpk M|Vv%y%Na߬jl]B%vLxɈ}λ[U=%㽉 v|Pj%6 /_VW2paG?+KՎ+Tʓ%gږGXK^ev#~+^ H"6HNK*ɳu/{ ߓŰ|J~,HTw6iirKv1oю;qery\K&xĻj<:p[O?rܪj}?*sO"u 2N܅g{ ?w͠j9Wэn8o�KB>N?yKɨ0 2nn!'ٍޚJVXCDX5CFXVľ:q9%!]CZr'Łb\m5yErd>(q9;UN,CN ){'uy}$""""C"m 0cv5k#TNYGŕ?%∬]'p{q曹Lg rHӑHi *$>3,ks5}3cO$(!RKŒ;w0|]NYy1.xCi<\n&t닊ya]=Cuf0xۣ#g$Hu,! 7mSeFn6oY!2+\rVbJp7$i.f`U-~uBwWmo X֎ֵ:IH㺱5\c9s4v L,Czw>> {i%w?0A afWS)#!1>Rz`Jyvy`틼 qT7 )E3 Ĺ8\}>65(i5+"mWeA.O'I_L kpՐę%@yU5s6dsbreS3R80R}SXqo*~&.`!""""/}-%i!c sK4?G!iڬim?J 5J SJ SJ SJ  0$%(-5~3!"""""MD!6}x /=QT-_?;mְ^{S扳'O26|QșY75v o2R){;~[~"""""C"mX2"g|p rir\O#F軗KB=1aUS<+25C^>KWݑo_#>1W\>_c @4jHԞUvJ wV/TGe}氮Tʖ<T>\|1qc)|k�Yl(.˺p L ݽ>#;OO|$-QAM ڂ6@ QKH6XY<75e|f8_r@hxw-&?3,Xä)<ԃӴ%eene-)1'""""[HKa\N#cI-Pm3Q'UӿKr kql�îcܚ,r">90oY;n:^a|p{EJuID{.W=wXekOѫKXT9;N`zSU,**<lXwcW.H䭹FP͛s(Mz?B~I,w"lB!aC4Vb?L| n&ZvMj5Wo(aռ8z7)yfϰKYv^$1aթֳ~"""""C"mPBjGeCmcx } =TD- iQzz澗F9bNN掱=WҭorB%C+?cXWP$HΦvd<+xvl rhW_MfEԳ*DVlD<*|?Wϯf$& Yb&o8(o8UL  dֺQ:7oKࡧ8 "m0^K(Ƨ0Zi1i ?ߎIt}z#uFRts +$̷G<oܕzzODDDD5SbHͱ>eKwRç_ǰhll+*SeaVyt1|;sB'!l2'?]`Q~{5G[E$.M'ur8wSx}p*"dq'iWOv"7?/B?r(eٮb+cy~/nۖ/*౓!gj2wO:7<~u箏nNd!R.=1) 9Kbۺ%p+9evuy?i?iR]l$^g1rфx#?ز^c aLJC%'Zc Q䦍s] {XU6V0s;D,qT1. ƥ3zrz9cDfxD.~˖B/D#gӲǮVrщwu)N=ADDDD6J A&9#Or9>\T${K~v%j8vw7}^N9~So.s}|R"eCjٴmbZe $u:#%ɱxU>_k \ Tٺew+<ni|K)/kh́.ID!kX O_a jHZUWVF#""""J A"�ˋ$&{ ;l܍n<%aIpq3.8rڻe$Hc6, KfjC�I8,-5۴wG[U8#.{Czml<l)vzdU>X{1gi 5C).;5Æ̣myrX/WbHDDDDdgٓ\C^q?%^֬oS V>!ĀevX6=?/7d<U~|?u u((@E#4KzݛYT6#^Kz,qUcII9mY Wsv6xi{7%w.NZ}R!~>'1YjfXp-~]#OONDDDDZ%DڠfK9yh�5r~ô>6:(T ~wÒ^<gW2$LR&.ٽ'r>;ESS ;ϱה5Qޛ%gstw^ 6?˲(ǞQ}bٿ90"s˿a]} ['wuLG|_<jI/\F_]0W)݂8J$_ì~"Wp󩆏x) C|i66LJ!ac-Ct[9H ĐHKa 3>LemCCǓ2z˩0% w0BJn:%%r7&wr/Sx&~]1*{~^==筿%*Hc}LXXONHVM7xtH-ɔa1[5Q7߈{%Pe8_-S$;Uwyqrذ*Wư.4ϰrN/K&̩lեv+cO/"R/ f$qYU6~½N-3]r(^geI3|~^&7AWP&""""|i <R°Ds+B޾;s}͓o^*CU룱V >'"0 ;TDڠ+*9|-9z(q$UsEX_zҋ7n/cH Gij.8GįM& sCE\øWRxk7W쑿jY"?gy }XWژ5Ȉ3tpu ^Lαqn7:O]MxLscDk>{,BH+d"kD,^O{){6~"""""@SRhMj8+)$""""(1$"""""""N)1$"""""""N)1$"""""""N)1$H) "#گ4~""""mW/Ҧy#؂R8_7 #O#-~1We^6['u0Osgˏf&z2p[Bj9 `q.S?aY1nn~ {ͧ3-`07syC8O ; 8ZbWF~Iy8LMɔp>oZCK*~;""""C" Z�� �IDATmsw&ﰅ-zn5,`!Ѐc.=S? [|2O,1[?8q4S;~ď-: wjB8(oe̽q }Gt|D\lnž7O6޸,"oa-^8p/o~-Žsᤖ4UcN`+0>[ iݔiC7Q-n c* n#y}c5S5ׄ1 e@A _cǯexԙDXn)S;;x= P~L -Ź7Oy{sqo]u~ 50+T”Lt\s0NJֹ)wxIBH%" QAPRk]Qb **,xED( �)=.%BO'!Z'!<OGrήίiLșa_IY;$ NuVl=T?>_i�� {N>1{p^ Ɓ-_E</4݊xssa3,ID^7֔df *T\`_[9U0o]S/waـiY "}n1ƹ6 7m}wv_62( fa2151]b S�9ع|έe~AK<oW;6`ǜ !pam/Ź# ńع Z8=kBPDž,n,B۔�S:GmLHpa&W`  !N·x:MpQswpؤ0h69Eǩ`߸c:WY~PKWGٹBz.x<LI!94)Hĝ{"09W&nexL,6;a%{^LD88/7_|T<|)F/fW7yvb0^bNHVi!.v$_?Bo'өN+Ic\̛@v#=8_S>b/Uӣλf1W\烚=8WÌXR`ďW.fX}̢U-pn|{G5k|i yf[ ٺTM|T镘o.�L0-z=J\lDתb"7`3J~P>O/ocU>O`LrvW)_Qϯ!gLrn xkaX/l rqc6Ú ixzGb<[ׅ9nP/__hS~oy*vnQc$`f5#aG2m3xA;vC<Wi쪭.vG 0#io}bC 9SgvO8Σ5a2 pmB8%||'発 +qZ}}8ޯ 6b{|4Ĕي{CHN,JkzcZ`}RN]e`-OHDoVl4cL3yǘe_ q`־mb_C"OC/i90k\ Nᑚl bS 0HqӉ- p`>(g<ϧ SSNE_צ{Xi!:sxjMr*:S`쓕]( ڇ 5ǟThϋU! uo}<ivxMWoT"nǙcw(()+(n?]iO.=Alf \ A6d0y<aYO>|ߋM˃z_hh6휍xzi<6],$׋ŀg@,CRy|,u<ayXZfﴨ4 1uLƘaI҉S"}v\/1im{~pWW?j%c|I_qίiFBhw�v'U;;zA)73㾻?o PÇc`AFn^};;0Ӭ8=ﰵ`,1%،$֬ݝ6aJ4~9ž5 wB(TA6P)8ʥQ9ظmϷS '|sֱԭ&H=⦸~er\%~~9&ݫit b>SSOs~EDDDN3 D8VN1Uؔχ ?xCTeL� rSVcv1? LȴP11;k! \mlO�h]n*ٷ"~;R<80?m9"+Nӱy878ӲJVc|Nh PwiPa D[J2vJkܜAVYj-Yx?O}g'~8""""C"g ; 61;ObC0%sw0UJkaz2 0ۄ;rGwl0/زe1mc.v�,΄Ź�w6* obڞn`HFF܆;.Hϫ+c' kkݷ$fa~^4O&]燳tPc,~gxY ؿw acvޞGvo8[*c-D*A΋0]kcM_3c|y >χ q"Spda~2Rk?C"rٹxg,8݂!BV.6.rv3s~MLP7{hzL\ds+pW#Ub6Ɖ@jvPJ񃡼dWW<[gh}H߉8Xmz83,ƮޓxDh9l5q^<Bރ]�cK#m߄{.̽1]ӳN=U`G15sCۊ78dkk1OȁRC`m#Sp^LZș(,Wc\)͝7\{ߚ}?ޱx<:wac;NnGmOzS?Cq~K.qnYVWkbQ98_^3+N~~p (+98j3/ֽ2 <s__ sm%̅v4u,؀rhɆrAp۷=1*bVc*p> ̢6F4LLd)Y�N0>?/8NUp= Bb&EC{/6F ՅO;7cEa .qZ`]s8͸oI0W=/55Ww+"""r JSD@|]"!Bda4L(9rq !?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)9]V ՗X`yCe]""""""RLRPQ1;koC23sO\Q؎i^2WiB1+Ǵ\Fh3ZMDDDDDDN!bUx} Fl*Q2j"gg,r,Q;}S,$""""""'GAT+Ӯ9kL ,C*H"""""""rFRDS1˶Xyu4|U+.0ks>p0dמFfMcpㅡ4aԹgŴ,H[Ե>V:)WVGKbff~F27_t~PyW^<V0c9_ĕp= ^Y?E!DDDDDDDNu~N4mE=lBorG¢#% {0d\O*^d[+?W$I/&%[�B[Ao?΂2UaI-4rL|?~IۙS<z>?|K0xc:43޿o>[wP[y_k~㼥R} I5mZ8ܡ,_&""""""r S0#*ܜ]Azd}=s)N Aݩqy�XwK9L uϧ\|>e{g*c_e_l9 lx W-} 3�<5+4?;gs=/_Qh%]\'KMjB""""""":M% $2$ XϘ_^s101Miv:uyn3b�o|!eYܑ7:]NsZ {quI ?!ER.z)41Mhr˂o~aK[8M'tW\HDDDDDDO 9rs,6u"X6zZ^>&<0ҢtY9lD8ww<yU>�nma/Q,N:VΡrwb%x0&1;7T!$anqkJ"""""""rzTHL.{r��|LX!lz =iZw'oK,eԋ64C6w瘒l}/giک>sWr؎b7!D �YҪ~~* '<rs9,BY6%YKiвYq$=gs&GʪҭXR m@S�X89y1˶(_YS |omI&)4,$䒗sd꒟ odL8?Ųn.lH*IBCЭFfMIqCb{{{jQqM'cxvZv`0⍩ڏ!C+8›:S76/-zhI;_N`~UG`J&""""""rJ`ȄPwĬ*K^n.Pֳϻf [<|g|/=f&iX ;-`(p+/<rQ!ٺl.yϖK|-^<E/n ;3X=P.q<דO #b 3vC)(USObᶺ8B"""""""|. 9^c~i:z5uzkxsz7~ge_sKۯItՆ""""""RtZ|a5-d˶ sݘ[NיAp ,�,9,#  '8`DDDDDD(:c8Dս4㼳)sK}noe(9 Na 0[] YcY/?S&""""""ŧd""""""""~JsQDDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`HDDDDDDDO)S DDDDDDDD!?`H!<* s&WфYΣS-"""""RJ 9A|?m+ѐC;aAvANHE02#Ż`S (#2\䛼N@263"4=>סR;|¿!]y7/䤿)F{Rx{Tj�/Oɉ˘a" } 5nX^i ,c€ M?ZY͏}S{mmw/gˇCl(t<ïLJZ f{jndWqǼFc#"7[h8 8i|G_y$fP7Dӥ?`L0viGťYUndLv|Kʈmre?Wb|{waCz�/jDxs_bw7;苸w/^[>BAsxk{܊d18=,Ou"pKf]yedVYyu75co3$jwŽ"6+ر7姼ˋ1$q#$)>s{̵d$yRQXSɂdRyӵDKYl:ڻp) y&.ã]0oϾ_/Ǵ/ :tzで~#`ł4BO~ X1yUy~?R׸.7y}Ai248I~s:86oK&lF{bʢ\ɾn[gm7{6f'vD^#[wnrs3 ؈ь߶~Ln0Ч_LuY1-u5 3uD C0k*ro`JJ}6C[DL}3ŴhG =s~PFP'yot:SW0{5sNWbeq{RڭN&S/˦M廑6?0̲㧏>O5lon 6/c~rFo™oQrh5p4/fnj'a/sEA@}z.\Ō8tv[m]q])Tna;_bQ д]}+<O`^n-^e>_oaC6,woKHI9Ղ`ȸT1;yW~H^^L7%) ti ,(CYթBYm<ln3n^ݛ\JឤBӃB⦦/\vNBr2rǁrr"MwOywٰ5=q~z=}~e}Q֞Sv-?8ɞ-XI^,oXoHQL{ݸG\P(xDaTgθ>Q[?|d7<8ΣYC�ɗĜl` 3&n͹4d<3vn4nZ [a5Ҡ`}'K_dz˖8/uVƳ>дyu{EدۡP SSîIfuG!O :N5pOXź{]-]a\<T{>R0,U,?l[ْc)Lbu9F\s]YF6iOxN _\#Yv.NK(f}}^z~+Ͼbİ_Y=`8ǛIF내tvr.QQG/︧kx#{Ph`] ^=U{ۯqt5UlEU*S?|^.-bˁ?o`3Ujôh@N�� IDAT4m=n̘ ԩ}5!`u+:pKVZ&֘'2}DKr5Wu{>vϛOwݽEDDDDDJ@4 ɺB،X,_Hٽ�yyT9 AWiJfNIԄMc jy-W%|ܯo|?Kڌ<}YK|l<{=%p=1T@*8KckӞ7w\yӁ.f�( M.vYl.1o4S}O^?߷.#6i?DHE iq vj\s3siA.רhlr2iI `7y+HY7Ob0_[lz)QT ԝ[DDDDD?" هM_?i{Cطuww<y W]Rq֕ W4l?:v,BI2J__8o&q? p`z^]JϤSr&Lv8<urZӹccJ^vm'amSLTfIKad*wy/w!fpwuHCۦ`5ㆯ M44\'LbYn1Ob<y$3Q>Fe$('aſXWDDDDDDJJ42Lu9ʊ%{H)/me3zu]}fhnS \.8D@L]ʣ  !_Hd+j}}w'4 K]6I♹Uk]Խ0s OPt⏶~˞Ed-#[_v ӆQrx|A14Ԃ?zL_NWrxæ#l_:ÌI͸t⽻KdhYJp [5_YÒC|q JUF&"""""RRMe֒`AVaO'asy/:%жya9I;\+W$h|U\YcC-qͼ8"J^M̀b]Bǯѕ&lgӉ<%!?'4*k(SZ:u:_gEC~ÜpSâOs]pmTOVD6`]|yp&7QN4 Nfbl1ˆ+ɹ1 c#I̙Ovdqmi7؄><Min-u!Icj` DDDDDDJJލ;Q1Xmۅ-I`?I(�pȌ?)i7nH uooOm62*g'vbj</๞{yLs62Т)28_'2Ҩ`~}<>[Tǫo:P|#:Ə-#qO5n+Ϛw`A\t _W-5 5ic?[w/|G_|<bYѾSP0>-:,6m2?~|9CH]Td1\o<ca'|2oEťAhP[WM'cxvZvx',i^̶O˒~?tO=Jxa<ճSIp5u=?>+Ge;{ ̯J ;\QV\wJof-"""""CXv@vy7^*{LbӢ;/vZP}]W^x,B<xwu\F-187jCMt <&ryK(=+2Ι3peh~}=>_[e۬d|WNTp>;773pBO)yعnoW)739ywYA۶УW{Y=S f%/HacY`,k?>^f^%sѭVz>ϏJhVVNaV&3E4.[?#^"@XP33H[15ۏl?w|uW=߮$ցfɈp=ip˓dO|w{6xsDI>R{$bޞoz?6r$~i7L-1$"""""RbLZf?Ʃv7C`c3eiީ!Q™"^Q-yiG\]9眛yk\8:JX""""""%7.WƕΡJ i9O^ҡ1cS^ 5f ޚƼFӔ/x? KM No3GP㽶eӠ.t|q Nyj|_fuD>忌-b.A7%+!a{vmt(}�v*)iJ& ըJ s2mlIUsAo 0|kҠ`HDDDDDDDOi!?`HDDDDDDDO?B9 ����IENDB`���������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/themes/runtime-assets/images/�������������������������0000775�0000000�0000000�00000000000�15202323100�0027151�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/themes/runtime-assets/images/favicon.png��������������0000664�0000000�0000000�00000003166�15202323100�0031312�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR��� ��� ���szz��=IDATXkpW9B$$)D$H-&0:L;L cZ@D Qg:h -? VfT6[ I$osisY{][gi{o +A"@_1GZ1~'AN E/z^ S#vE#vn,Rr?jF;^�g]JiA5EyKU9#vD'2�]Wk#5"B$zwF>;3iD>Cmy=5Mz8Uū;d|{mJ޻BMkBKTr}?ENf!J'NU EbMhI}KcQh#k7qv1&yIoߚК c8t[ u"G9am4bר*?1r]=UEyu6(zވ-h[~ua3lIɞ~9dڴh0^6ueքXވ-DorQk7Shp)=yTcyy("sUxwϼ�sk#n<<_z# [x * -PUN78JᔿHpNFTclנj;\]}gqSU7mGQU:wy:{�d]<\rc{p~sƵ_q)̨�=7SCj BI5a 'C6Qڻ)⼥)~{c\~I\Zp6yH<w?y2fcM_9ŏ;5idw/׎}5}FNp 4`[c$G9>EADȦU~b+j6X<^~K-P-F,ք!�Ia Co;jsu>C].f y٥):{=W긺$y8A!: SZ9>iK!%b7 h$05a3'<Ʊšd=U8PYYj-p,7TSl9 ),z-޻vvL6e{DVl�Sl#1xE PIr2 Gmz+_)7u|mff5Pt1�i٬>j;8q%rgdՔKV1{<Z/rUm'#<[FtDE7_M43%٭l=뾰�ɐ .oy}bw>+^}{uoF3)Uo;p~T'QYWԎlkˋTƄVUZ.ā;X]ʲwDƘ"rU^[LnΙ4վsQ[ͤg #vExmm#G޿u䭖>WS<Uu U ?hǴ&p᭷5s"!A +_`"uWDl|<x  J����IENDB`����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/themes/runtime-assets/logo.png������������������������0000664�0000000�0000000�00000052555�15202323100�0027366�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR���������X��U4IDATx]|UNٚM=!{zwֳlXγpv,gGEzHo?,lB3/$lvg̼8S8vq p `{O^&��8��Q~xxn�VB8Ee @ �9<=LV^ "^~5�P @Mփg�~ +.T)iuH."Ҡ"" 6<X.<(B?#^^/s!<9:9O;;<V+<6+Vc<tXd?+�?dzv"H@H0$�3� 1 ÑIoGdzE ?ZS8xcX^ HDA$$ e�g`^?y|N<N55/܅\vj8 "!!�=͉S) `<!nO5%HMG\Oπ>2ڰppz=$,Tk #`_ಙnn0,h*9ڝp;Df+5�v8@PoG dA~AH990C ID{z'*Xn70vvTmۄ< �~�@~VɄ9$LFVpkrҐ=s919EFAz:tRe4/Q_@M�>NF^6�t}t#SƌGs Fo$H'+txT|�VPeCj� ILn�熧]ss!ouIaPaP /%QA�+eKlMG_&x�s\8btLis}Y0)btTx(_ -A_ƆU�`Da\`L͙}&F^W w"E0`y 'FƵ=I�N90ړ(��';I� /ZU]:5Ly9gm=k '`I ,K${K]]9&vXܓCEd[�y� iuɩ&SJVc0r(W8]fs$ ?*orQ[ ɫIr!Ɵ]^:Bp>r7DeetU,1RZ0܌0RQG] $ /ֳ<N),9QYADjt]pT0:l(\>x[h:X|`۵Y'&LE1o01b6|T UQG *7ohmxZ97ͧQz <��8HY@.)*0h#қIq *L>kaGlr{Ѵ5<f Gn^p8P_ط%e$;k ~`9\o5;Y$Q$+ifix㏟{6D^U%g� Y&f5 bqegEedECtN`9< Gm !.5} � b\& ݖ6qJo`SNaR a Wcl50XLrY0L%&">0޶ Ek1Çcq4y2^!)\.PXXmX7UT1{ֹH; 004Ol{o` y.1UqɽX,+p5/pq "ZdG\{32΀!* P7kkDg5mUEtO<4%ij4#Z:4Yu{vՕL}{ A) `�s=077V s\":15xث;nF9cL4^Cw@Hm6<:ef? Ν&7w]_Iw<@r=X0 uܣ}4AȒ5aM6ebO6"K/>(K>bbi=eϚsD"e>P"Jer0f 1YHNIN!($nVس{76n܈m;w o\EtUT1{sǏ5c驈n:\Yc8TmހROĄ[!.[s<--ƆE�ޔ`2IB*֕$9m�;iOVeOɑ,lU\R?:\ɊcDWwG 3/܂[nQQQݸ4qm##hWH!0,KY ~篰 n�Gȩ <=_t}&:J~~#2g/n̅/<E�|"[b1!RNLI/x tK !7tocSĐ+;m!]emªG[Wz+n&u.= _GL!pd 4+|g{7|3vjsAW@xr=^8/7hM';1!p y.xX'KbޫIJy1:7@Q',5v ]!|3xՋ{oGqxpeh4v:O3gb7_a/?FN*Ж믿NI 5' qXمqx V1ٹ DDr*o^C+c|u;wFi%a,EBB%$陯Zܐn ^މ7CۅR{?�_{y)O:s pWy|mDxF܁V`ɒ%6mZF{صk>κ#F{UcURGIkDqܰ۽@T!HPKH9g=rƗÈׁ:!x}(x(p>\}V1118wFl܀:5yYyK~˗`[oaʔ)ABB >}2שqZ-brl/T; ۷ cteԿ-H0Ģdp66ɶGvZb$B(49?OAhf$6#QQ;ܼMqbգ!m*= b#/�<8j0L7~<^#<n<tT2rkb-9< "2ݏo:*+5k. Ռ l%Y&J7xnxaIO;:ױGgP(\*G]-ٴx,fLt?w`Y;xyv ?~eK/ 2'pt>_x1F<Q7to^Gff&Dc|bGl\xVTmt-Ȟy<0K^p,lt[ GGps =ȲXqu(ZjJ톫n)g_9'"KC3 W뼑4FՊqANa9 /^|Y5Jѱ(Aj*vOv}kHFE Tb ,;q@k4xלI Ìf;BHv~.$)% x CwxRBi19ٸ曻=5Y>$@oqw$d6qM83a-M'wN17M @ ZJ >|fLހ6F(U۷R^G "ұϠjwmǕ/; "Fo~DtN~' ^7>LAM׋{bÆ ؿ?hInn69gP1Z]] όO8]8<29-�Bu#86]၈ht`~c0i:>}:Mը)؊@UD9dz> oDjݢTo݄CdC|rzFl�exvQV˺v R4 tL[[Sr*F^{LI=ff͚cűbҥKD!0aN?tzغu?[% 0�۽8<V o݄l6qE'<n> :˲TTmL>v5itU4k7�+ylՇll`{Lwc}"Hk%Fu8 1ef'Gm-9ƍ;Fp:? V\IU~ /Ν7|N&5q&U]ISGIHڮz)=}r}xne1t9ncb$Ț~:;VSA#ߕ$La7$ \t{CEZ,X~z|Z5SҐ{yaBAx< >v'G]]nv j`ĉY^nn |}駟)5{a<E۔A^멫_vb\7j_ #Ti}7َ0ҫp{u`Qې8tDDVV_ffn2~NM|":6l`6XGc$J�xQUUsacS:N#9QZc W{gi}PGv^uB65,:>3 yyJC3NAmP<8MM;hhhŋQSSөwFk$J`% | $޷o_F,A5 {К">a -)Dq(_{>$JYd)b-2YWsSQD 39ς.A4"80EV.K򉂑g?, |A .!YGok֬իC8ղ   NǍI賭ڼ2 (x-p[�l 9$Hҳ�N}f&ت:nb7]U F Ԏ0~xj9p) ZChj:}dJhZf޾rCR%%%Q'""*++s^֜<|/m!bQ_2�?[b݅vU d@7,:}ڃbAwBMݻ]DWmnnn5LFӣCkSO= :q8n:FXR*uu4 [%UW_Qr/ C4ݝ571Qw-;Vu,G|{ՒVM 1,wܪUd:Y[вq`X췕H2C?͙Byu{J3ϸ)):,|1cдd 4}v؁K.ž{6q x}}ґ= jn{e7]v*hY&F\mJNW^zۖaΦF}lcܺ,c/Qm3-�5 Bn܌տs*A@$yXk׮m3Aԩj4v7⭷޺Fwe5%2~<ӏHBR:t ]veA6T8p�# g^wh^E^[xxHپ%JEtJE~?$P IfVIZ pɧJML�ZC]xοK, \eKGuo2$'cݺumFˋhbq nأ"lXdLթ׈ Tٍ_|F @>Vtt4Ć"X,ԥhs9= U< Gţ;$AoϷŇvu8Q%M\~,MB-w|;�67s}qXTvBh vaʕ-Ԑ@_wcbb`J5nقCH=ӛ#=<l+G3�A&!!'|BmFfy1c Ւ7oތϾ9sÅj:IH6VKr:Y2X>r$GHs+ I/`nC ƢKMpD{'�M(syoNqѕĸ߿?}Yax'K::1mUן1st&"QinC"j4Eatj`gASᗟh<6k>KJUTīD RQjYwQKRr*+8bO|ߺ$!m4[ش.d@&̅^H_=+V`MZ%zK_> /2VY9/hԆ#2#UUIK *$O0^p)yn Ԋ!сCV;l \tEի z= XߗC%eOKE`!c, %hؽPc[d^&IsP4-[| Q vg2)i4˵"dz)쫪YT=lCFP_ 8 RY%W"܋h}P̍i4Qr{ ->:<r^tN޴A]qG0GxR*6,L:Ux +>AC}1D[0[++и \rΦFhe9 w0~Pkk{M=j_gqq1>C<B 6 t `7_BpMVV8GL`/\'q?ձC撃X?1Фnǁ]AMM zI|䋈88bh M"{M"~,tJ0W߯ƐDy9s& P)=l\N|uD IZ$k eBmAme6_1? BCL,į|Ϳ^*8XVQO>G ep57'puuwfUn\K& 4b4wv[=CRR{,Y+.~&<F^ǫxXfV$[dU}Lmjd1>]#8wdM/AQK/>qڂh^0 U|hega5|H9H˜6 I`uxPRr^gTT휨t`W^]w݅^S1ǐ2v*Ukp<O[Fz d<XSkNDZ!WX2']14l~^}Q7vX\ux>q&0"2Z+mܕlSfQ?LExRYXX v{N'Myp=? 0ǐ;\*UhWn+])bS j$N!6^ 3)J-{:gpp '⫯py8o<Z@;ٻd زy3t8g6RF1-OКDU}+ KLCQB"3Qg']X|d )S9'nɭ'",# 1?^OK裢h>2_)idkR>:*I3kh.".ysC7^ƣ=iS ]!{¿^w/+/GʸI!N'P{FX!.q 戗L$^C1H;o " u8~5jJKP\ufjxT8bCJsO;|`] q$cY0A]j @SX1;Qn5͍Svv6bconGccWUk4!,2L;EzY0 %Cݖuݎo 3`5:e̓Ey\/M7lk #oUf=;JS|^Z_(Kc6$H&v$R$TS9]"걥jH^5MȺ`pȭE4ށC?#S?28kjx>it c壷bG漋|ڜA}[f]@ @H*b,AA$Qj$XC\\HbLhvyl뱊e1;}fC\L3Ћ몖Nせ.߮> Z{k/~?gMrpzMDUSR 8QAEy5,5 \."Jbf]A8':2Qi܉0+L_ cXmm3萞dcl:D$Ř`xy._s#"9c]u]YҨ5wU ZSyo<.a dbퟁQiQM8T^U0[D$E .:g"6:&=|>BƲ7%0uL^C|1~d I)^0/;D1wDvB Uϊsf 0?ˤH$'Da->Wop_/73=v5~i =v?O2Tvmzݜ Ay5\j63ݟB ]8{pfv%H%DE;J΁|651w̟ CC9ʖ~AY}Ndb$&D} D+ρCLoDBDa߾ ڵoubd RF"*WbOxa$Ć}r Κ6 Ѭ ; *:-n0Q&F(ITV }N' [`9;[^KKXBrbiHg$Dz܉C`Yra XNGJ# <^Vmh4aw$XKY "͊~W$%$8^NYbb DM̆^g<$$S`8 bH^jr΢Xj'6l/FiU#]㣐icqa ' v*SٯӽVz;$JpZt ҋ#H9`]xk9HKDZM ނƒUؼe/N;_/EJF-Ki%ÛW#7=> f#>6<B#&8EX21h5}F$>F˩&\ЋQ2&AX0lYZ#<;?~]dDj)ID�uV4F13_}2p9n'Fcc#6oڄ\x O(N7~Y[pƹput$@$FAX}IMr1[J4lߌM?@?nÅ_6uH+GEE:KWll,̝;Q߬ŗcn3 }Y+Gї!Cf_LA#`+ի&A׫z& 6:)D]y-y^vC墅;*Ml< xCmaa.ԍ݊ Ͱ2Y5 Dy] wfqq0gcVCCݐE$H{!}\\$n>|jѷH"GqzDt磕U㸶V+ݤEԫ>KOHHqCRׁo_@GZS FC{osEz}X=@ISSR n9$.:uu-3B"B뉈Bn 2Ifk`$!@z=lG *!IGh/bqGG{%mWYju: Dstv.y&=Չfu\.>u: YC8w#D 1qȚ97lmڒ86m(**P[[ h4}oOM'imCr #J6KQ~5ףl}-1Yx1+ŋK͛71^P8qwF}  11F|s<c.1w܉{x!dλg]0x鍷{QÞ%??3fCy2_wE%B$[镕M$?bn7^SxWCj1Gh 2g<4V|]?<.":xIw=ܳ4.-hq[,QhU ,njGu`݌7#y8H]d4]z%?핿|ٳ ,!`$K+E&AFWsc *W@|9; Q2ISgdƓٽ.IӎQXr,wRNOrKH*Ԏ b=)$NFr2 O!`$HB U4{l6r<M>&P}n7} n߶@~ei@ih mZF9#c >}Z-mKjjP�BVt@w<hD>ץW0�)<D FON1>(nߏ>�i>jU^H]sՍX`D0`Wdj@|#h �XRFf=(XuN[ <ε6YTZ#p&eB<YCG[+_=Nb�yn;&D r5Qo/vz#HJm, z8^H֖X3,Kc Y@ /�lM?ZAI] <ХO@҈ ˣB`VJV�j5 ?0BŁ˱'u�H>(2Dں! ¢ H,ut1G;U1}OD ljAS2VbL7ߏ B IIq՝q5<L  9ă/QClN: 6vU,E>A<nhGSQ,eviprlwZ+2&c5Aa.ػ{ ð+]d(\S*Vro!\^^ѱNo@� a�22Gz,x{_Y ȿ#slzaRYx^@ȃE] ^cW7!}KMwb Qwu7ۮF=`ޟ!̰Gj4OBm $5Mtozol z}*؂__CJ*\7/c9b,v%%ksU(_y!rι YY$R<h_^ �Y_ɤۇjfԬz٩b a?/wΗY$Ulcq#w5uR aLyW["ip[< `GPޕ1{F߯*U댈c1 缁-O_G]]P�wKi=>'=f>N ˅ODY\|QILJbpQiqީ6H mx},rW]MMX}&F߇թN!6lޅ%|=o=rNN^GM%%=l1&'R=bY~쁇Q pQEmb2yz fuЍS=@IpOPE>k9Oaiz+ Z.7cX|z8z4EH2X G'#F:^S O0bJ-"NTA[Qn% =6Ӡ`b v=Vt ۳ތY6#m@l( u#^ p64kZr ҺAb9%Q.&(à vz'FM S)btD=ۋq7b'HV0рom5S:x%K`ܝH;EH8CM"W Rҧ MyTjŖO!7urGRB gY׬TUM4` W@Dr*LG.�i_ o楓 7 ϣpJ[{% HW \Q @Ǎ/BeLK]"H`QQs*˝>@m2u; ` \K|^=ixD]$1>%,Tt j*�(�@VGC*K`�C�0 1@/JRJm"cw0C|gHϊyv$AÙ0+YipxPe݂_#dhh!@Z>UUc�fh g?,'ٖTY8}-�k�|�`0ݑ.$�8]`Ġla2aPf,vDa% F܈9C-b`_P`YiQSkQSÁ#N,$ Nݼуp k)rů; v\'< `Lp x�k5{9(IqQ0Γ$Itqk dDϧ/]pQcL(vBScJ-ћ18J:-~.FEHb`];0tDȏ�`؝n+.�qt\rx,ef_[_L^ɶJ]<)c\b?qqJ^""DQ:Z.r0(_/ND6coAc+bX5HI$`fl}_F4L@k0s 2|NKN܏W6dM!�aAM@;wn7;uop{BOۂUu!'-dцX%= Eq,2R֡bO!Us&ab~� @:c̟(AxS]k^}4D :phu�,ÿ}H b!5rRTĞ+E&G+: R IR.d�sм"NCD$̶E QoEjb�n0%Q ms妎둕 KOz|x}A$|h|Z>FM1)|n{P#ݍcs}-zM tI1Hߏ?AX4Z d$sYq5!��DV$9jXΛ(21 CvDx<>.&rJ gEhpۍX}QXw,WpXs IauUQMeƖ+8Wz>B1�%,}Y}tn_RAdӀ$JCN(N1|@&,ˠ;ߏraD?$D.AՊ?"=I<kU,y^d d,EyoG[n,-gTċDIPs3ƸxjwΓyһ1'Up=},̢<!UDܼN[EӺX*3s~LcQQՀ&+2tz?A3 (nDlJiix;z^]$ZKy!봳B|t 8Y؜HӵI<tň4a_aKQ`v~9]%`9Od :3ϧk$,F o/>1-!^$VRRIܽvC(zZxdː!>9i>xV5[;]m])Lr|5No�E; |3t3R{0u6ݶd64Yxzr_YU}(J �y qu:ވ P'4$~U龜( ,wX 9DnM2S${3y2Fi82&$DX.%;.a7D BWo3mEmض>[~kX)2@*o~d(@6 V,]t4HQԫ �كXH9^K“r˜n|s j{2)(P*wZnP2aBN>=hwOǚ?�~_f*YrEX^o|>4Fz| #G CRPh�0P:. )i!;&, NfZ7 +f~݆|9yF,-2)nyo'JIS mE.z+R8պ<^+r�%�˟o x۽)'<u{=̩?Z ӣ "SPDRZث+2ݗe9Jы!&K @l;y6Fz72}sDoX ^AYM'ˬ%GC\MM`%Ey<u;K3,XT.g҃H=!t%1ҹ]VG^^Â6U Z;u 6eWȌ}˒75yn*ns=4AU(L4/^B*xEh1@ˇ˞ uƐB Yc4hy9(YXbn RuxdX:e|c?C&rZ-1]c ^1YC݂ ⡷#!HF(Q|y+ "²_:@.y~1\ $$A[\ȁ .`g0̽2Yg"4p:]0p6 2F�jͳaH0 &(D|MʞD0p-Έ ,]ZS-SK)64AJe! Ķ"Ok8$BA뀪WfGk㍱qnn^aDĈ&*3f_qz(u#�Pc3~Z6' 8."Aٸ\y,ϕK&M16!d#?�ĠA&wW@g ۀp] &gOa1ʫ[i^V0gޅA B"amm�9_\gROj`;B"O(B: jaLzy(I,_ʖc"ģӎ .gOC֟+"\z=6 z]QEL )<DǔFz@gcۨ2M$2FEgu p 9̛=kLf2=k{N_#֯h$!&?#Qݬrv9|##4JPþ/597,RjRx|?Ɛ-QBȕ$aIQ LM iF}dTSvsՃz{!֕>Mm`.M$ar D߫y<n1*5b<f.`wP%ȱ`?JgGy-\6N2O C9rD={a%R@9$5ѫ? 5TWnk{e-49S.*ezxSDVVj?(N3袢Ta硂KxVibd ~P'U:/?#ˍ= g|aڄnD@eBkyĥQ[ŊnXtgS$BGﻖ7a|ԃu{n_{foEMGl m"v|j˶#Kܕ^';6YPҤ 7   %HkWoȠ6A룢)AHn>5׻bFAۤK?¼No<V9ظرy:y2l;sG R=APbo. =^g*:tr)&A8^s!@u]$\\cKBoF-DH2;<u2BCrUSѝô"ABDˑJ4XjG`$!~ie$79.ID$nV[Ö-b& Rԉ "ƙXPZ=ŹPAC `>!`VRC 6A4a&jOsI!& F>a 4<�-gQKwu�!AY/hrRJ`t`B,/uB5 %FcH6R`d)f@Vjv(X1rQt| ҦYTqL.aPAu!!+)T zNcC%r5p|"\]И*J^u8%nܪ2@cmi4a&px511`"W4Qk}k8v*nY" b:br'T,nv u=" E}uZmfYuLu "A%:6W LI+pL I$YxC 6?D< nXkG7L /j2 vK5 PǷ&A D+* "sL m;֧pzBt޷266H"Ď+܄ Ĭ<G3ў!Qn(VC]a^ PV=Za5- ݉ITG|AV/Ox ˆi 3E$1hVዒU)OS7gt-T@"IRdq�>̐mj G]I *EI=0zfbzL`㧠Tf*RA8cL3%UTb(JI#)w8T_ EI+p ҧtŢ>E]ץ!UC�,"GS)�~^s˺"tSJ bI[ X<0ZV@eIBܠ'X a�^X� >ZY'A@^11&/Bq2D hmQ7`Y`A$^24[B0 ʤU8^b8CgcOKj:C-Zh-Ŭc\ڒ'PּJ}KqE1Sɦkfp5IK.+"?'_1A R[ET2CQDQ2P}Pω]zH'eiȑb =6oM-U+]‡HtmXrHH/u ۥ)ЅG sU8t bդyܨ*f2(ዽ�#(HzI)#�Um^)$RǎgɆ!o DPֺ;ަ8eq?U;m6+!IV:AE83 G#1*:.,sE,Be{$_]*&rp[lrX!}j$ 20Bs%x kB31s[Q32cNGi(\Y3 *D� ֠Ҽ fg1!'xvm*Z@[G+#]C]]x(2N[Ÿ=<tݚ$sx밵|xkѠglK#LyC]!Հ(Po3Hj]:dPS# qg`XCOJ8s0e8?RwǦT2%\5\EDn3m;lU0]KM$HJfB! ?ۡR< cPv }P ]Gޖ|^8(AAOxqI:! 3݁U?'$|F݇XU{68j1[]XvIE.fbǏFi=EzV->FҘ ,5 u8lhjBM8%Irzj/#l\ayMwjk sdX4IكZ8"*$NT,c}#o md)osK#t@$4/$=7_&AQemXZ &l<Emؗi_k7 :J^BNK7/\ zݣغ|U%{Y?.4BȾ)cE@掵0yfuAƹ7$kh%FV""=cnE՗c"'6>e 2;7"J&6VqB]v65{zR͢$ڧ};v~YI v _1�)G{@0h>X)#lU䨫q=["Xm!(\7J Kt8`}c\I3zEawD@L@>bXu^[EDeÀᗏyJ8\2_e;y pˤpC#a?H4aaȘ<L|}؄d)DF{/hH-sU872]�i% yكvj+%PM:U풼s.Ő;`KU&6wU_ASBFkC?�bPd $4U=";(g67C%M^C6jUjS"EEUU(j&Ғ "BcC1ul/{ww63ՙ=C)PPؙ1O:=3>y.9ٴ%!ސnQ-Ǣ [ڄ p (-3B)P֯AC]Yt%\2W^D91QZ0JADvev 0*9CpPkRq3? }{^BǗX(ڷa`TBD;9X۰Wa^4ou\Bp¤b#MR~Z''|T=i4.פdf 뾵mҩ#lhYaN Q$PDLQ%M0}OoƬ&Ϲ"LPKB V.>h#m.cמ;8 ,0FJE J˩eP0 5Ƭ d7,|z#]=0 [b{G p0�7 c|鎳 �ޡs񘡴J?]PfT/[Q6ObVlJ`ŧX̮a! 57?l'0z~oNT"q�Q0,i+"RW}ZU=aa4Z ɑ]%[Q AOA bI,n\Ǡ W".Loa^icD+E8lAJ\0m}gZ0@.-;(4BNCBԽiPXѵyg:kYFO zi~Ns"Xh' s4fѸ 0fg10&7._0&� H(09"Ln@G^qJ1"a~LNqo 6Zކ>韽ZF( #MEAD*A*CEA6n41a BdJ��`cXRYΪƂJK:?N<qDg\6�"�\�8%\,P:}²`F7(^͂Msanio1hZwneHAf|aY"! ̝Խ"nU ٫(\/G&s9}H%bs 1µq7(#q#7KC]DzW.KC�^;M}N` %~sR!Fl4'c&y Z]?rsXU/Et Hǎ-+Eh+Gu^~]py@ۋ̫/":y%@rZ(*h-84{*25Яb"Y1?k!CFZZt dY>b)% b>db ԕJnXVǟ(z"rӢ .DykX1,r表(-kwӿr?,Ѓ@e1֔Yܠðp>F.0L@,uT-փ@@}J{6_L-[* l,S?}�+$fp.04 E [ ĻZu&?@ߤFZ jAjvE )D_a1k^߿[o"ϿG+vT^Zb2vjWVy db{oUԸD9)K8kˤHQ ٱfЛ@@ >Z\߸!CYes*r;a$—�tVY^E졉k%75Ĭv% 5mF 0, o xqd�Qf`UN*BoB$ĬZcng:˺ k(tvD*�]_(-W!զQ 28R`d2a [`>x`h^td23Tr:7Hq_C= T$~�IJwWiI [W]PD*@|ͯ'v Q1EWFY bĈŧSͮs8knYRcs,rZ~?0vcW<MӑD5F)zI2PE~ M[D\g v]>ÿ}XZlπ EW8I?J]]+ xq~+^x)lkoo�'{0gqIo Vx}e3X=_mW{�d8`uԄ8g ���7+6@L����IENDB`���������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/themes/runtime-static/��������������������������������0000775�0000000�0000000�00000000000�15202323100�0025671�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/hack/doc-site/hugo/themes/runtime-static/github.png����������������������0000664�0000000�0000000�00000006734�15202323100�0027673�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR���������>a���sBIT|d��� pHYs��v��v}Ղ���tEXtSoftware�www.inkscape.org<�� YIDATxiU�ffP`,AH,bR(.hL*fAHYh4hqĥ\hLL$ "Q˥$"0'?yͻꚁy}۫*uji`4�!@4tI�G*Uݘ!PHo(Ç??I;ʼ,DU<O � =Ix;8V`aVL�N 읮ExxXX)�C)9A)S.k;U)E&@Drq)T=mc�^d໸\5ꎴ  "}UWjf506&�<*}2U}8M#R �9�9T i(%PDL5W̠)LDk�-),^&겤&Vȅ3ԝ_FlSE�=fJׁTu&@DN8 < & "SkAz{� 6|In /-8Ll>(ZIs3� J�,^څQij\ eP_:g#QD�9X�Uy/lƩsQ)�DdO%`(Jx-`~չ;?K|(A3�CpbvMl�zu:7?؆ yrpS3g1- 4'ۀcM^haw ]p $vZjۓC��c<Bi=k��#q 4d XqGڀ50Y.Ƶ+~)x�X{kӞyiӻn~ P#H <J p2"|>!oy2<En;nv+7<h15#2ЕQŃ=j�dx`( êeKp` n! M(ܙ"'`YpuE?CDyO'Zn!SJgс VVcjbPlZ˂ k|Sl2وeA @pR/dj"��>F`ǂFr{R9�EO;9PvcY63 m>-TL�14PvcY6| dB'uTyn%�$cȉدk p6tCttpnF[LZgWMHϮ@0`Hif5J .Z@E T㖍Y0Xf`X�YǸh؎_`қT:*kQUDdPF U]kjV>T_5?Qzc5㠳 kh3pje.nc3�*FX$Y[WC�r!l(yP ?ĊAȕkŁ9l םU}xPEsE e O>̺Z s 5]+X9l~wPXacz�Dz k�ue6Z^k24ovo%bYM9lz@|,p3N�en=�,Skz �\iCٵFݵ5ADDdM�xcx;0*W3eg�8UD jƼ�n,{jd$p;v+7.6`UNU7vZ2)u&5KUucGdcU}$t �Y"/=|6UO@g�,ĝa؝>ZM\ dXy2Hchkո*,Y#`"h�?I@Or ·<_'�� Dd$&`-Vw$/ӈW;vgWa{LL>FD\S'9p>&dcQSԙ)Dd^쎉/]Ajrg@ߴjL@~A}XNڊ;m)`\kFuQi;'Mz7͞q lǽ&6(&>?c�ƾRp;n�AAkcp3pDڎ37xݫݾ"F-d|1 1 ӛp_Rvz~z�GK% c$ѸhOڎc̳pwYJl/zufcwڀ)"pU6/k%"ZU<I0ق[ۜ�>aשR".w�zxR憐%aW8S&d[#(5Я^F@u/N[nmفEa>Oq[4%9pl9i-W�2{^DL6y4~G'~쑶{J{a@pbQ oyI@`3Ri!EPdhOl;0D`=)� $ VV>BdxVH,!s/`I̮Ny/Ȁ kC]cŠS{!�N5v�A'D42d_[F2x+a 8k*2uxBNq'/ 'wLZPp1D?ܫ\ ! "kXcftx8IUR� H a. 0 FZnDF0l}>N�ډӣ8bDUNP#GՙA>bpU@ c!m >]U,7<=+{wC|J{Ƀ p`ڎ璉 #^0u1zf<|_D¾Qd4{q~iOŏS |7v&ϡ0)8@OToyڂ{۽ Ԕ&yO⬃6bUŷ`��q )]M/"?gq0IUM.6{Uq)K|p887llE` |7y O^qt0M5|ĎջnQ p;c}˳0.U)z&rbO.F�DJ5�[qD{YnW՟S=Ad`^[= EjUu<U*-`-Uu/’t& n"ZPi5VyCUv&��P~U.ƪ>ꅚRFn^`\  yT#yjm=iMpHɑ 2`ߔvm}=L�T`RUtr(txomY{b ySx����IENDB`������������������������������������go-openapi-runtime-decad8f/headers.go���������������������������������������������������������������0000664�0000000�0000000�00000001215�15202323100�0020016�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "mime" "net/http" "github.com/go-openapi/errors" ) // ContentType parses a content type header. func ContentType(headers http.Header) (string, string, error) { ct := headers.Get(HeaderContentType) orig := ct if ct == "" { ct = DefaultMime } if ct == "" { return "", "", nil } mt, opts, err := mime.ParseMediaType(ct) if err != nil { return "", "", errors.NewParseError(HeaderContentType, "header", orig, err) } if cs, ok := opts[charsetKey]; ok { return mt, cs, nil } return mt, "", nil } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/headers_fuzz_test.go�����������������������������������������������������0000664�0000000�0000000�00000003101�15202323100�0022127�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "net/http" "strings" "testing" ) // FuzzContentType exercises [ContentType] with arbitrary // Content-Type header values. Invariants: must not panic or hang; // when err is non-nil, the returned media type and charset must // both be empty. // // Lens 4 (header parsing) of the security scrub: // .claude/plans/security-scrub.md. func FuzzContentType(f *testing.F) { const appJSON = JSONMime seeds := []string{ "", " ", appJSON, appJSON + "; charset=utf-8", appJSON + "; charset=\"utf-8\"", appJSON + "; charset=\"utf\\\"8\"", appJSON + "; charset=\xff\xfe", appJSON + ";", appJSON + ";;", appJSON + "; ;", appJSON + "; charset", appJSON + "; charset=", "application/octet-stream", "text/plain; charset=us-ascii", strings.Repeat("a", 4096), appJSON + "; " + strings.Repeat("x=y;", 256), } for _, s := range seeds { f.Add(s) } f.Fuzz(func(t *testing.T, in string) { h := http.Header{HeaderContentType: []string{in}} mt, cs, err := ContentType(h) if err != nil { if mt != "" || cs != "" { t.Fatalf("ContentType(%q) returned (mt=%q, cs=%q, err=%v) — non-empty mt/cs with error", in, mt, cs, err) } return } // Success path: when input is non-empty and parses, mt // must be non-empty (the stdlib mime.ParseMediaType already // guarantees this; we re-assert as a regression guard). // Empty input is allowed: returns ("", "", nil) via the // DefaultMime branch. _ = mt _ = cs }) } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/headers_test.go����������������������������������������������������������0000664�0000000�0000000�00000003154�15202323100�0021061�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "mime" "net/http" "testing" "github.com/go-openapi/errors" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestParseContentType(t *testing.T) { _, _, reason1 := mime.ParseMediaType("application(") _, _, reason2 := mime.ParseMediaType("application/json;char*") data := []struct { hdr, mt, cs string err *errors.ParseError }{ {"application/json", "application/json", "", nil}, {"text/html; charset=utf-8", HTMLMime, charsetUTF8Val, nil}, {"text/html;charset=utf-8", HTMLMime, charsetUTF8Val, nil}, {"", "application/octet-stream", "", nil}, {"text/html; charset=utf-8", HTMLMime, charsetUTF8Val, nil}, {"application(", "", "", errors.NewParseError("Content-Type", "header", "application(", reason1)}, {"application/json;char*", "", "", errors.NewParseError("Content-Type", "header", "application/json;char*", reason2)}, } headers := http.Header(map[string][]string{}) for _, v := range data { if v.hdr != "" { headers.Set("Content-Type", v.hdr) } else { headers.Del("Content-Type") } ct, cs, err := ContentType(headers) if v.err == nil { require.NoError(t, err, "input: %q, err: %v", v.hdr, err) } else { require.Error(t, err, "input: %q", v.hdr) assert.IsTypef(t, &errors.ParseError{}, err, "input: %q", v.hdr) assert.EqualT(t, v.err.Error(), err.Error(), "input: %q", v.hdr) } assert.EqualT(t, v.mt, ct, "input: %q", v.hdr) assert.EqualT(t, v.cs, cs, "input: %q", v.hdr) } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/interfaces.go������������������������������������������������������������0000664�0000000�0000000�00000010424�15202323100�0020530�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "context" "io" "net/http" "github.com/go-openapi/strfmt" ) // OperationHandlerFunc an adapter for a function to the [OperationHandler] interface. type OperationHandlerFunc func(any) (any, error) // Handle implements the operation handler interface. func (s OperationHandlerFunc) Handle(data any) (any, error) { return s(data) } // OperationHandler a handler for a swagger operation. type OperationHandler interface { Handle(any) (any, error) } // ConsumerFunc represents a function that can be used as a consumer. type ConsumerFunc func(io.Reader, any) error // Consume consumes the reader into the data parameter. func (fn ConsumerFunc) Consume(reader io.Reader, data any) error { return fn(reader, data) } // Consumer implementations know how to bind the values on the provided interface to // data provided by the request body. type Consumer interface { // Consume performs the binding of request values Consume(io.Reader, any) error } // ProducerFunc represents a function that can be used as a producer. type ProducerFunc func(io.Writer, any) error // Produce produces the response for the provided data. func (f ProducerFunc) Produce(writer io.Writer, data any) error { return f(writer, data) } // Producer implementations know how to turn the provided interface into a valid // HTTP response. type Producer interface { // Produce writes to the http response Produce(io.Writer, any) error } // AuthenticatorFunc turns a function into an authenticator. type AuthenticatorFunc func(any) (bool, any, error) // Authenticate authenticates the request with the provided data. func (f AuthenticatorFunc) Authenticate(params any) (bool, any, error) { return f(params) } // Authenticator represents an authentication strategy // implementations of Authenticator know how to authenticate the // request data and translate that into a valid principal object or an error. type Authenticator interface { Authenticate(any) (bool, any, error) } // AuthorizerFunc turns a function into an authorizer. type AuthorizerFunc func(*http.Request, any) error // Authorize authorizes the processing of the request for the principal. func (f AuthorizerFunc) Authorize(r *http.Request, principal any) error { return f(r, principal) } // Authorizer represents an authorization strategy // implementations of Authorizer know how to authorize the principal object // using the request data and returns error if unauthorized. type Authorizer interface { Authorize(*http.Request, any) error } // Validatable types implementing this interface allow customizing their validation // this will be used instead of the reflective validation based on the spec document. // the implementations are assumed to have been generated by the swagger tool so they should // contain all the validations obtained from the spec. type Validatable interface { Validate(strfmt.Registry) error } // ContextValidatable types implementing this interface allow customizing their validation // this will be used instead of the reflective validation based on the spec document. // the implementations are assumed to have been generated by the swagger tool so they should // contain all the context validations obtained from the spec. type ContextValidatable interface { ContextValidate(context.Context, strfmt.Registry) error } // ContentTyper is implemented by values that declare their own MIME // content type. The client runtime consults it in two places: // // - on a body payload set via [SetBodyParam]: when the payload is a // stream (io.Reader, io.ReadCloser) and ContentType returns a // non-empty value, that value becomes the wire Content-Type // header instead of the operation's picked consumes entry. // // - on individual file values inside a multipart upload: their per- // part Content-Type header is taken from ContentType() rather // than sniffed via http.DetectContentType. // // An empty string return is treated as "no opinion" and the runtime // falls back to its default selection. Values that have no content // type to declare may simply not implement the interface. // // See docs/MEDIA_TYPES.md for the full client-side selection // algorithm. type ContentTyper interface { ContentType() string } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/internal/����������������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0017671�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/internal/testing/��������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0021346�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/internal/testing/data.go�������������������������������������������������0000664�0000000�0000000�00000043203�15202323100�0022610�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package testing import ( "encoding/json" ) // PetStore20YAML [yaml] doc for swagger 2.0 pet store. const PetStore20YAML = `swagger: '2.0' info: version: '1.0.0' title: Swagger Petstore description: A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification termsOfService: http://helloreverb.com/terms/ contact: name: Swagger API team email: foo@example.com url: http://swagger.io license: name: MIT url: http://opensource.org/licenses/MIT host: petstore.swagger.wordnik.com basePath: /api schemes: - http consumes: - application/json produces: - application/json paths: /pets: get: description: Returns all pets from the system that the user has access to operationId: findPets produces: - application/json - application/xml - text/xml - text/html parameters: - name: tags in: query description: tags to filter by required: false type: array items: type: string collectionFormat: csv - name: limit in: query description: maximum number of results to return required: false type: integer format: int32 responses: '200': description: pet response schema: type: array items: $ref: '#/definitions/pet' default: description: unexpected error schema: $ref: '#/definitions/errorModel' post: description: Creates a new pet in the store. Duplicates are allowed operationId: addPet produces: - application/json parameters: - name: pet in: body description: Pet to add to the store required: true schema: $ref: '#/definitions/newPet' responses: '200': description: pet response schema: $ref: '#/definitions/pet' default: description: unexpected error schema: $ref: '#/definitions/errorModel' /pets/{id}: get: description: Returns a user based on a single ID, if the user does not have access to the pet operationId: findPetById produces: - application/json - application/xml - text/xml - text/html parameters: - name: id in: path description: ID of pet to fetch required: true type: integer format: int64 responses: '200': description: pet response schema: $ref: '#/definitions/pet' default: description: unexpected error schema: $ref: '#/definitions/errorModel' delete: description: deletes a single pet based on the ID supplied operationId: deletePet parameters: - name: id in: path description: ID of pet to delete required: true type: integer format: int64 responses: '204': description: pet deleted default: description: unexpected error schema: $ref: '#/definitions/errorModel' definitions: pet: required: - id - name properties: id: type: integer format: int64 name: type: string tag: type: string newPet: allOf: - $ref: '#/definitions/pet' - required: - name properties: id: type: integer format: int64 name: type: string errorModel: required: - code - message properties: code: type: integer format: int32 message: type: string ` // PetStore20 json doc for swagger 2.0 pet store. const PetStore20 = `{ "swagger": "2.0", "info": { "version": "1.0.0", "title": "Swagger Petstore", "contact": { "name": "Wordnik API Team", "url": "http://developer.wordnik.com" }, "license": { "name": "Creative Commons 4.0 International", "url": "http://creativecommons.org/licenses/by/4.0/" } }, "host": "petstore.swagger.wordnik.com", "basePath": "/api", "schemes": [ "http" ], "paths": { "/pets": { "get": { "security": [ { "basic": [] } ], "tags": [ "Pet Operations" ], "operationId": "getAllPets", "parameters": [ { "name": "status", "in": "query", "description": "The status to filter by", "type": "string" }, { "name": "limit", "in": "query", "description": "The maximum number of results to return", "type": "integer", "format": "int64" } ], "summary": "Finds all pets in the system", "responses": { "200": { "description": "Pet response", "schema": { "type": "array", "items": { "$ref": "#/definitions/Pet" } } }, "default": { "description": "Unexpected error", "schema": { "$ref": "#/definitions/Error" } } } }, "post": { "security": [ { "basic": [] } ], "tags": [ "Pet Operations" ], "operationId": "createPet", "summary": "Creates a new pet", "consumes": ["application/x-yaml"], "produces": ["application/x-yaml"], "parameters": [ { "name": "pet", "in": "body", "description": "The Pet to create", "required": true, "schema": { "$ref": "#/definitions/newPet" } } ], "responses": { "200": { "description": "Created Pet response", "schema": { "$ref": "#/definitions/Pet" } }, "default": { "description": "Unexpected error", "schema": { "$ref": "#/definitions/Error" } } } } }, "/pets/{id}": { "delete": { "security": [ { "apiKey": [] } ], "description": "Deletes the Pet by id", "operationId": "deletePet", "parameters": [ { "name": "id", "in": "path", "description": "ID of pet to delete", "required": true, "type": "integer", "format": "int64" } ], "responses": { "204": { "description": "pet deleted" }, "default": { "description": "unexpected error", "schema": { "$ref": "#/definitions/Error" } } } }, "get": { "tags": [ "Pet Operations" ], "operationId": "getPetById", "summary": "Finds the pet by id", "responses": { "200": { "description": "Pet response", "schema": { "$ref": "#/definitions/Pet" } }, "default": { "description": "Unexpected error", "schema": { "$ref": "#/definitions/Error" } } } }, "parameters": [ { "name": "id", "in": "path", "description": "ID of pet", "required": true, "type": "integer", "format": "int64" } ] } }, "definitions": { "Category": { "properties": { "id": { "format": "int64", "type": "integer" }, "name": { "type": "string" } } }, "Pet": { "properties": { "category": { "$ref": "#/definitions/Category" }, "id": { "description": "unique identifier for the pet", "format": "int64", "maximum": 100.0, "minimum": 0.0, "type": "integer" }, "name": { "type": "string" }, "photoUrls": { "items": { "type": "string" }, "type": "array" }, "status": { "description": "pet status in the store", "enum": [ "available", "pending", "sold" ], "type": "string" }, "tags": { "items": { "$ref": "#/definitions/Tag" }, "type": "array" } }, "required": [ "id", "name" ] }, "newPet": { "anyOf": [ { "$ref": "#/definitions/Pet" }, { "required": [ "name" ] } ] }, "Tag": { "properties": { "id": { "format": "int64", "type": "integer" }, "name": { "type": "string" } } }, "Error": { "required": [ "code", "message" ], "properties": { "code": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } }, "consumes": [ "application/json", "application/xml" ], "produces": [ "application/json", "application/xml", "text/plain", "text/html" ], "securityDefinitions": { "basic": { "type": "basic" }, "apiKey": { "type": "apiKey", "in": "header", "name": "X-API-KEY" } } } ` // RootPetStore20 json doc for swagger 2.0 pet store at /. const RootPetStore20 = `{ "swagger": "2.0", "info": { "version": "1.0.0", "title": "Swagger Petstore", "contact": { "name": "Wordnik API Team", "url": "http://developer.wordnik.com" }, "license": { "name": "Creative Commons 4.0 International", "url": "http://creativecommons.org/licenses/by/4.0/" } }, "host": "petstore.swagger.wordnik.com", "basePath": "/", "schemes": [ "http" ], "paths": { "/pets": { "get": { "security": [ { "basic": [] } ], "tags": [ "Pet Operations" ], "operationId": "getAllPets", "parameters": [ { "name": "status", "in": "query", "description": "The status to filter by", "type": "string" } ], "summary": "Finds all pets in the system", "responses": { "200": { "description": "Pet response", "schema": { "type": "array", "items": { "$ref": "#/definitions/Pet" } } }, "default": { "description": "Unexpected error", "schema": { "$ref": "#/definitions/Error" } } } }, "post": { "security": [ { "basic": [] } ], "tags": [ "Pet Operations" ], "operationId": "createPet", "summary": "Creates a new pet", "consumes": ["application/x-yaml"], "produces": ["application/x-yaml"], "parameters": [ { "name": "pet", "in": "body", "description": "The Pet to create", "required": true, "schema": { "$ref": "#/definitions/newPet" } } ], "responses": { "200": { "description": "Created Pet response", "schema": { "$ref": "#/definitions/Pet" } }, "default": { "description": "Unexpected error", "schema": { "$ref": "#/definitions/Error" } } } } }, "/pets/{id}": { "delete": { "security": [ { "apiKey": [] } ], "description": "Deletes the Pet by id", "operationId": "deletePet", "parameters": [ { "name": "id", "in": "path", "description": "ID of pet to delete", "required": true, "type": "integer", "format": "int64" } ], "responses": { "204": { "description": "pet deleted" }, "default": { "description": "unexpected error", "schema": { "$ref": "#/definitions/Error" } } } }, "get": { "tags": [ "Pet Operations" ], "operationId": "getPetById", "summary": "Finds the pet by id", "responses": { "200": { "description": "Pet response", "schema": { "$ref": "#/definitions/Pet" } }, "default": { "description": "Unexpected error", "schema": { "$ref": "#/definitions/Error" } } } }, "parameters": [ { "name": "id", "in": "path", "description": "ID of pet", "required": true, "type": "integer", "format": "int64" } ] } }, "definitions": { "Category": { "properties": { "id": { "format": "int64", "type": "integer" }, "name": { "type": "string" } } }, "Pet": { "properties": { "category": { "$ref": "#/definitions/Category" }, "id": { "description": "unique identifier for the pet", "format": "int64", "maximum": 100.0, "minimum": 0.0, "type": "integer" }, "name": { "type": "string" }, "photoUrls": { "items": { "type": "string" }, "type": "array" }, "status": { "description": "pet status in the store", "enum": [ "available", "pending", "sold" ], "type": "string" }, "tags": { "items": { "$ref": "#/definitions/Tag" }, "type": "array" } }, "required": [ "id", "name" ] }, "newPet": { "anyOf": [ { "$ref": "#/definitions/Pet" }, { "required": [ "name" ] } ] }, "Tag": { "properties": { "id": { "format": "int64", "type": "integer" }, "name": { "type": "string" } } }, "Error": { "required": [ "code", "message" ], "properties": { "code": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } }, "consumes": [ "application/json", "application/xml" ], "produces": [ "application/json", "application/xml", "text/plain", "text/html" ], "securityDefinitions": { "basic": { "type": "basic" }, "apiKey": { "type": "apiKey", "in": "header", "name": "X-API-KEY" } } } ` // PetStoreJSONMessage json raw message for Petstore20. var PetStoreJSONMessage = json.RawMessage([]byte(PetStore20)) // RootPetStoreJSONMessage json raw message for Petstore20. var RootPetStoreJSONMessage = json.RawMessage([]byte(RootPetStore20)) // InvalidJSON invalid swagger 2.0 spec in JSON. const InvalidJSON = `{ "apiVersion": "1.0.0", "apis": [ { "description": "Operations about pets", "path": "/pet" }, { "description": "Operations about user", "path": "/user" }, { "description": "Operations about store", "path": "/store" } ], "authorizations": { "oauth2": { "grantTypes": { "authorization_code": { "tokenEndpoint": { "tokenName": "auth_code", "url": "http://petstore.swagger.wordnik.com/oauth/token" }, "tokenRequestEndpoint": { "clientIdName": "client_id", "clientSecretName": "client_secret", "url": "http://petstore.swagger.wordnik.com/oauth/requestToken" } }, "implicit": { "loginEndpoint": { "url": "http://petstore.swagger.wordnik.com/oauth/dialog" }, "tokenName": "access_token" } }, "scopes": [ { "description": "Modify pets in your account", "scope": "write:pets" }, { "description": "Read your pets", "scope": "read:pets" }, { "description": "Anything (testing)", "scope": "test:anything" } ], "type": "oauth2" } }, "info": { "contact": "apiteam@wordnik.com", "description": "This is a sample server Petstore server...", "license": "Apache 2.0", "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.html", "termsOfServiceUrl": "http://helloreverb.com/terms/", "title": "Swagger Sample App" }, "swaggerVersion": "1.2" } ` // InvalidJSONMessage json raw message for invalid json. var InvalidJSONMessage = json.RawMessage([]byte(InvalidJSON)) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/internal/testing/data_test.go��������������������������������������������0000664�0000000�0000000�00000000423�15202323100�0023644�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package testing import ( "testing" "github.com/go-openapi/testify/v2/require" ) func TestInvalidJSON(t *testing.T) { require.NotEmpty(t, InvalidJSONMessage) } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/internal/testing/petstore/�����������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0023213�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/internal/testing/petstore/api.go�����������������������������������������0000664�0000000�0000000�00000012145�15202323100�0024316�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package petstore import ( goerrors "errors" "io" "net/http" "strings" gotest "testing" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" testingutil "github.com/go-openapi/runtime/internal/testing" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/runtime/security" "github.com/go-openapi/runtime/yamlpc" "github.com/go-openapi/testify/v2/require" ) const ( apiPrincipal = "admin" apiUser = "topuser" otherUser = "anyother" ) // NewAPI registers a stub api for the pet store. func NewAPI(t gotest.TB) (*loads.Document, *untyped.API) { spec, err := loads.Analyzed(testingutil.PetStoreJSONMessage, "") require.NoError(t, err) api := untyped.NewAPI(spec) api.RegisterConsumer("application/json", runtime.JSONConsumer()) api.RegisterProducer("application/json", runtime.JSONProducer()) api.RegisterConsumer("application/xml", new(stubConsumer)) api.RegisterProducer("application/xml", new(stubProducer)) api.RegisterProducer("text/plain", new(stubProducer)) api.RegisterProducer("text/html", new(stubProducer)) api.RegisterConsumer("application/x-yaml", yamlpc.YAMLConsumer()) api.RegisterProducer("application/x-yaml", yamlpc.YAMLProducer()) api.RegisterAuth("basic", security.BasicAuth(func(username, password string) (any, error) { switch { case username == apiPrincipal && password == apiPrincipal: return apiPrincipal, nil case username == apiUser && password == apiUser: return apiUser, nil case username == otherUser && password == otherUser: return otherUser, nil default: return nil, errors.Unauthenticated("basic") } })) api.RegisterAuth("apiKey", security.APIKeyAuth("X-API-KEY", "header", func(token string) (any, error) { if token == "token123" { return apiPrincipal, nil } return nil, errors.Unauthenticated("token") })) api.RegisterAuthorizer(runtime.AuthorizerFunc(func(r *http.Request, user any) error { userStr, ok := user.(string) if !ok { return goerrors.New("unauthorized") } if r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/api/pets") && userStr != apiPrincipal { if userStr == apiUser { return errors.CompositeValidationError(errors.New(errors.InvalidTypeCode, "unauthorized")) } return goerrors.New("unauthorized") } return nil })) api.RegisterOperation("get", "/pets", new(stubOperationHandler)) api.RegisterOperation("post", "/pets", new(stubOperationHandler)) api.RegisterOperation("delete", "/pets/{id}", new(stubOperationHandler)) api.RegisterOperation("get", "/pets/{id}", new(stubOperationHandler)) api.Models["pet"] = func() any { return new(Pet) } api.Models["newPet"] = func() any { return new(Pet) } api.Models["tag"] = func() any { return new(Tag) } return spec, api } // NewRootAPI registers a stub api for the pet store. func NewRootAPI(t gotest.TB) (*loads.Document, *untyped.API) { spec, err := loads.Analyzed(testingutil.RootPetStoreJSONMessage, "") require.NoError(t, err) api := untyped.NewAPI(spec) api.RegisterConsumer("application/json", runtime.JSONConsumer()) api.RegisterProducer("application/json", runtime.JSONProducer()) api.RegisterConsumer("application/xml", new(stubConsumer)) api.RegisterProducer("application/xml", new(stubProducer)) api.RegisterProducer("text/plain", new(stubProducer)) api.RegisterProducer("text/html", new(stubProducer)) api.RegisterConsumer("application/x-yaml", yamlpc.YAMLConsumer()) api.RegisterProducer("application/x-yaml", yamlpc.YAMLProducer()) api.RegisterAuth("basic", security.BasicAuth(func(username, password string) (any, error) { if username == apiPrincipal && password == apiPrincipal { return apiPrincipal, nil } return nil, errors.Unauthenticated("basic") })) api.RegisterAuth("apiKey", security.APIKeyAuth("X-API-KEY", "header", func(token string) (any, error) { if token == "token123" { return apiPrincipal, nil } return nil, errors.Unauthenticated("token") })) api.RegisterAuthorizer(security.Authorized()) api.RegisterOperation("get", "/pets", new(stubOperationHandler)) api.RegisterOperation("post", "/pets", new(stubOperationHandler)) api.RegisterOperation("delete", "/pets/{id}", new(stubOperationHandler)) api.RegisterOperation("get", "/pets/{id}", new(stubOperationHandler)) api.Models["pet"] = func() any { return new(Pet) } api.Models["newPet"] = func() any { return new(Pet) } api.Models["tag"] = func() any { return new(Tag) } return spec, api } // Tag the tag model. type Tag struct { ID int64 Name string } // Pet the pet model. type Pet struct { ID int64 Name string PhotoURLs []string Status string Tags []Tag } type stubConsumer struct { } func (s *stubConsumer) Consume(_ io.Reader, _ any) error { return nil } type stubProducer struct { } func (s *stubProducer) Produce(_ io.Writer, _ any) error { return nil } type stubOperationHandler struct { } func (s *stubOperationHandler) ParameterModel() any { return nil } func (s *stubOperationHandler) Handle(_ any) (any, error) { return map[string]any{}, nil } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/internal/testing/petstore/api_test.go������������������������������������0000664�0000000�0000000�00000000453�15202323100�0025354�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package petstore import ( "testing" "github.com/go-openapi/testify/v2/require" ) func TestAPI(t *testing.T) { doc, api := NewAPI(t) require.NotNil(t, doc) require.NotNil(t, api) } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/internal/testing/simplepetstore/�����������������������������������������0000775�0000000�0000000�00000000000�15202323100�0024425�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/internal/testing/simplepetstore/api.go�����������������������������������0000664�0000000�0000000�00000020472�15202323100�0025532�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package simplepetstore import ( "encoding/json" stderrors "errors" "net/http" "sync" "sync/atomic" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware/untyped" ) // NewPetstore creates a new petstore api handler. func NewPetstore() (http.Handler, error) { spec, err := loads.Analyzed(json.RawMessage([]byte(swaggerJSON)), "") if err != nil { return nil, err } api := untyped.NewAPI(spec) api.RegisterOperation("get", "/pets", getAllPets) api.RegisterOperation("post", "/pets", createPet) api.RegisterOperation("delete", "/pets/{id}", deletePet) api.RegisterOperation("get", "/pets/{id}", getPetByID) return middleware.Serve(spec, api), nil } var getAllPets = runtime.OperationHandlerFunc(func(_ any) (any, error) { return pets, nil }) var createPet = runtime.OperationHandlerFunc(func(data any) (any, error) { asMap, ok := data.(map[string]any) if !ok { return nil, stderrors.New("bad data: wanted map") } pet := asMap["pet"] body, ok := pet.(map[string]any) if !ok { return nil, stderrors.New("bad pet body: wanted map") } name, ok := body["name"].(string) if !ok { return nil, stderrors.New("bad name: wanted string") } status, ok := body["status"].(string) if !ok { return nil, stderrors.New("bad status: wanted string") } return addPet(Pet{ Name: name, Status: status, }), nil }) var deletePet = runtime.OperationHandlerFunc(func(data any) (any, error) { asMap, ok := data.(map[string]any) if !ok { return nil, stderrors.New("bad data: wanted map") } id, ok := asMap["id"].(int64) if !ok { return nil, stderrors.New("bad id: wanted int64") } removePet(id) return map[string]any{}, nil }) var getPetByID = runtime.OperationHandlerFunc(func(data any) (any, error) { asMap, ok := data.(map[string]any) if !ok { return nil, stderrors.New("bad data: wanted map") } id, ok := asMap["id"].(int64) if !ok { return nil, stderrors.New("bad id: wanted int64") } return petByID(id) }) // Tag the tag model. type Tag struct { ID int64 Name string } // Pet the pet model. type Pet struct { ID int64 `json:"id"` Name string `json:"name"` PhotoURLs []string `json:"photoUrls,omitempty"` Status string `json:"status,omitempty"` Tags []Tag `json:"tags,omitempty"` } var pets = []Pet{ {1, "Dog", []string{}, "available", nil}, {2, "Cat", []string{}, "pending", nil}, } var petsLock = &sync.Mutex{} var lastPetID int64 = 2 func newPetID() int64 { return atomic.AddInt64(&lastPetID, 1) } func addPet(pet Pet) Pet { petsLock.Lock() defer petsLock.Unlock() pet.ID = newPetID() pets = append(pets, pet) return pet } func removePet(id int64) { petsLock.Lock() defer petsLock.Unlock() var newPets []Pet for _, pet := range pets { if pet.ID != id { newPets = append(newPets, pet) } } pets = newPets } func petByID(id int64) (*Pet, error) { for _, pet := range pets { if pet.ID == id { return &pet, nil } } return nil, errors.NotFound("not found: pet %d", id) } var swaggerJSON = `{ "swagger": "2.0", "info": { "version": "1.0.0", "title": "Swagger Petstore", "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", "termsOfService": "http://helloreverb.com/terms/", "contact": { "name": "Wordnik API Team" }, "license": { "name": "MIT" } }, "host": "localhost:8344", "basePath": "/api", "schemes": [ "http" ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "paths": { "/pets": { "get": { "description": "Returns all pets from the system that the user has access to", "operationId": "findPets", "produces": [ "application/json", "application/xml", "text/xml", "text/html" ], "parameters": [ { "name": "tags", "in": "query", "description": "tags to filter by", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "csv" }, { "name": "limit", "in": "query", "description": "maximum number of results to return", "required": false, "type": "integer", "format": "int32" } ], "responses": { "200": { "description": "pet response", "schema": { "type": "array", "items": { "$ref": "#/definitions/pet" } } }, "default": { "description": "unexpected error", "schema": { "$ref": "#/definitions/errorModel" } } } }, "post": { "description": "Creates a new pet in the store. Duplicates are allowed", "operationId": "addPet", "produces": [ "application/json" ], "parameters": [ { "name": "pet", "in": "body", "description": "Pet to add to the store", "required": true, "schema": { "$ref": "#/definitions/petInput" } } ], "responses": { "200": { "description": "pet response", "schema": { "$ref": "#/definitions/pet" } }, "default": { "description": "unexpected error", "schema": { "$ref": "#/definitions/errorModel" } } } } }, "/pets/{id}": { "get": { "description": "Returns a user based on a single ID, if the user does not have access to the pet", "operationId": "findPetById", "produces": [ "application/json", "application/xml", "text/xml", "text/html" ], "parameters": [ { "name": "id", "in": "path", "description": "ID of pet to fetch", "required": true, "type": "integer", "format": "int64" } ], "responses": { "200": { "description": "pet response", "schema": { "$ref": "#/definitions/pet" } }, "default": { "description": "unexpected error", "schema": { "$ref": "#/definitions/errorModel" } } } }, "delete": { "description": "deletes a single pet based on the ID supplied", "operationId": "deletePet", "parameters": [ { "name": "id", "in": "path", "description": "ID of pet to delete", "required": true, "type": "integer", "format": "int64" } ], "responses": { "204": { "description": "pet deleted" }, "default": { "description": "unexpected error", "schema": { "$ref": "#/definitions/errorModel" } } } } } }, "definitions": { "pet": { "required": [ "name", "status" ], "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" }, "status": { "type": "string" }, "tags": { "type": "array", "items": { "type": "string" } } } }, "petInput": { "allOf": [ { "$ref": "#/definitions/pet" }, { "properties": { "id": { "type": "integer", "format": "int64" } } } ] }, "errorModel": { "required": [ "code", "message" ], "properties": { "code": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } } }` ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/internal/testing/simplepetstore/api_test.go������������������������������0000664�0000000�0000000�00000005670�15202323100�0026574�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package simplepetstore import ( "bytes" "context" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/runtime" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestSimplePetstoreSpec(t *testing.T) { handler, err := NewPetstore() require.NoError(t, err) // Serves swagger spec document r, err := runtime.JSONRequest(http.MethodGet, "/swagger.json", nil) require.NoError(t, err) r = r.WithContext(context.Background()) rw := httptest.NewRecorder() handler.ServeHTTP(rw, r) assert.EqualT(t, http.StatusOK, rw.Code) assert.JSONEqT(t, swaggerJSON, rw.Body.String()) } func TestSimplePetstoreAllPets(t *testing.T) { handler, err := NewPetstore() require.NoError(t, err) // Serves swagger spec document r, err := runtime.JSONRequest(http.MethodGet, "/api/pets", nil) require.NoError(t, err) r = r.WithContext(context.Background()) rw := httptest.NewRecorder() handler.ServeHTTP(rw, r) assert.EqualT(t, http.StatusOK, rw.Code) assert.JSONEqT(t, "[{\"id\":1,\"name\":\"Dog\",\"status\":\"available\"},{\"id\":2,\"name\":\"Cat\",\"status\":\"pending\"}]\n", rw.Body.String()) } func TestSimplePetstorePetByID(t *testing.T) { handler, err := NewPetstore() require.NoError(t, err) // Serves swagger spec document r, err := runtime.JSONRequest(http.MethodGet, "/api/pets/1", nil) require.NoError(t, err) r = r.WithContext(context.Background()) rw := httptest.NewRecorder() handler.ServeHTTP(rw, r) assert.EqualT(t, http.StatusOK, rw.Code) assert.JSONEqT(t, "{\"id\":1,\"name\":\"Dog\",\"status\":\"available\"}\n", rw.Body.String()) } func TestSimplePetstoreAddPet(t *testing.T) { handler, err := NewPetstore() require.NoError(t, err) // Serves swagger spec document r, err := runtime.JSONRequest(http.MethodPost, "/api/pets", bytes.NewBufferString(`{"name": "Fish","status": "available"}`)) require.NoError(t, err) r = r.WithContext(context.Background()) rw := httptest.NewRecorder() handler.ServeHTTP(rw, r) assert.EqualT(t, http.StatusOK, rw.Code) assert.JSONEqT(t, "{\"id\":3,\"name\":\"Fish\",\"status\":\"available\"}\n", rw.Body.String()) } func TestSimplePetstoreDeletePet(t *testing.T) { handler, err := NewPetstore() require.NoError(t, err) // Serves swagger spec document r, err := runtime.JSONRequest(http.MethodDelete, "/api/pets/1", nil) require.NoError(t, err) r = r.WithContext(context.Background()) rw := httptest.NewRecorder() handler.ServeHTTP(rw, r) assert.EqualT(t, http.StatusNoContent, rw.Code) assert.Empty(t, rw.Body.String()) r, err = runtime.JSONRequest(http.MethodGet, "/api/pets/1", nil) require.NoError(t, err) r = r.WithContext(context.Background()) rw = httptest.NewRecorder() handler.ServeHTTP(rw, r) assert.EqualT(t, http.StatusNotFound, rw.Code) assert.JSONEqT(t, "{\"code\":404,\"message\":\"not found: pet 1\"}", rw.Body.String()) } ������������������������������������������������������������������������go-openapi-runtime-decad8f/json.go������������������������������������������������������������������0000664�0000000�0000000�00000001201�15202323100�0017347�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "encoding/json" "io" ) // JSONConsumer creates a new JSON consumer. func JSONConsumer() Consumer { return ConsumerFunc(func(reader io.Reader, data any) error { dec := json.NewDecoder(reader) dec.UseNumber() // preserve number formats return dec.Decode(data) }) } // JSONProducer creates a new JSON producer. func JSONProducer() Producer { return ProducerFunc(func(writer io.Writer, data any) error { enc := json.NewEncoder(writer) enc.SetEscapeHTML(false) return enc.Encode(data) }) } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/json_test.go�������������������������������������������������������������0000664�0000000�0000000�00000002042�15202323100�0020412�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "bytes" "io" "net/http/httptest" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) var consProdJSON = `{"name":"Somebody","id":1}` type eofRdr struct { } func (r *eofRdr) Read(_ []byte) (int, error) { return 0, io.EOF } func TestJSONConsumer(t *testing.T) { cons := JSONConsumer() var data struct { Name string ID int } err := cons.Consume(bytes.NewBufferString(consProdJSON), &data) require.NoError(t, err) assert.EqualT(t, "Somebody", data.Name) assert.EqualT(t, 1, data.ID) err = cons.Consume(new(eofRdr), &data) require.Error(t, err) } func TestJSONProducer(t *testing.T) { prod := JSONProducer() data := struct { Name string `json:"name"` ID int `json:"id"` }{Name: "Somebody", ID: 1} rw := httptest.NewRecorder() err := prod.Produce(rw, data) require.NoError(t, err) assert.EqualT(t, consProdJSON+"\n", rw.Body.String()) } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/logger/������������������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0017334�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/logger/logger.go���������������������������������������������������������0000664�0000000�0000000�00000000710�15202323100�0021140�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package logger import "os" type Logger interface { Printf(format string, args ...any) Debugf(format string, args ...any) } func DebugEnabled() bool { d := os.Getenv("SWAGGER_DEBUG") if d != "" && d != "false" && d != "0" { return true } d = os.Getenv("DEBUG") if d != "" && d != "false" && d != "0" { return true } return false } ��������������������������������������������������������go-openapi-runtime-decad8f/logger/logger_test.go����������������������������������������������������0000664�0000000�0000000�00000000407�15202323100�0022202�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package logger import ( "testing" "github.com/go-openapi/testify/v2/require" ) func TestLogger(t *testing.T) { require.FalseT(t, DebugEnabled()) } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/logger/standard.go�������������������������������������������������������0000664�0000000�0000000�00000001073�15202323100�0021464�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package logger import ( "fmt" "os" ) var _ Logger = StandardLogger{} type StandardLogger struct{} func (StandardLogger) Printf(format string, args ...any) { if len(format) == 0 || format[len(format)-1] != '\n' { format += "\n" } fmt.Fprintf(os.Stderr, format, args...) } func (StandardLogger) Debugf(format string, args ...any) { if len(format) == 0 || format[len(format)-1] != '\n' { format += "\n" } fmt.Fprintf(os.Stderr, format, args...) } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/middleware/��������������������������������������������������������������0000775�0000000�0000000�00000000000�15202323100�0020172�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/middleware/body_test.go��������������������������������������������������0000664�0000000�0000000�00000005117�15202323100�0022521�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "context" "io" "net/http" "path" "testing" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/internal/testing/petstore" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) type eofReader struct { } func (r *eofReader) Read(_ []byte) (int, error) { return 0, io.EOF } func (r *eofReader) Close() error { return nil } type rbn func(*http.Request, *MatchedRoute) error func (b rbn) BindRequest(r *http.Request, rr *MatchedRoute) error { return b(r, rr) } func TestBindRequest_BodyValidation(t *testing.T) { spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) api.DefaultConsumes = runtime.JSONMime ctx.router = DefaultRouter(spec, ctx.api) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, path.Join(spec.BasePath(), "/pets"), new(eofReader)) require.NoError(t, err) req.Header.Set("Content-Type", runtime.JSONMime) ri, rCtx, ok := ctx.RouteInfo(req) require.TrueT(t, ok) req = rCtx err = ctx.BindValidRequest(req, ri, rbn(func(r *http.Request, _ *MatchedRoute) error { defer r.Body.Close() var data any e := runtime.JSONConsumer().Consume(r.Body, &data) _ = data return e })) require.Error(t, err) assert.Equal(t, io.EOF, err) } func TestBindRequest_DeleteNoBody(t *testing.T) { spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) api.DefaultConsumes = runtime.JSONMime ctx.router = DefaultRouter(spec, ctx.api) req, err := http.NewRequestWithContext(context.Background(), http.MethodDelete, path.Join(spec.BasePath(), "/pets/123"), new(eofReader)) require.NoError(t, err) req.Header.Set("Accept", "*/*") ri, rCtx, ok := ctx.RouteInfo(req) require.TrueT(t, ok) req = rCtx err = ctx.BindValidRequest(req, ri, rbn(func(_ *http.Request, _ *MatchedRoute) error { return nil })) require.NoError(t, err) // assert.Equal(t, io.EOF, bverr) req, err = http.NewRequestWithContext(context.Background(), http.MethodDelete, path.Join(spec.BasePath(), "/pets/123"), new(eofReader)) require.NoError(t, err) req.Header.Set("Accept", "*/*") req.Header.Set("Content-Type", runtime.JSONMime) req.ContentLength = 1 ri, rCtx, ok = ctx.RouteInfo(req) require.TrueT(t, ok) req = rCtx err = ctx.BindValidRequest(req, ri, rbn(func(r *http.Request, _ *MatchedRoute) error { defer r.Body.Close() var data any e := runtime.JSONConsumer().Consume(r.Body, &data) _ = data return e })) require.Error(t, err) assert.Equal(t, io.EOF, err) } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/middleware/consts_test.go������������������������������������������������0000664�0000000�0000000�00000002326�15202323100�0023074�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware // Test-only constants extracted to satisfy the goconst linter. // These are shared across the test files of the middleware package. const ( // struct field / map key names for spec.Parameter maps. keyID = "ID" keyName = "Name" keyTags = "Tags" keyFriend = "Friend" keyRequestID = "RequestID" ) const ( // lowercase spec parameter / route param keys. paramKeyID = "id" paramKeyName = "name" paramKeyAge = "age" ) const ( // collection format identifiers. multiFmt = "multi" pipesFmt = "pipes" ssvFmt = "ssv" tsvFmt = "tsv" ) // jsonMime is the application/json content type used throughout the test suite. // Local const (not the public runtime.JSONMime) so it stays self-contained. const jsonMime = "application/json" const ( // recurring test values. tagOne = "one" tagTwo = "two" tagThree = "three" valToby = "toby" valHello = "hello" valYada = "yada" valFragment = "fragment" pathSomething = "/something" valTheUser = "the user" testAuth2 = "auth2" testAuth3 = "auth3" testAuth4 = "auth4" ) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������go-openapi-runtime-decad8f/middleware/context.go����������������������������������������������������0000664�0000000�0000000�00000064000�15202323100�0022205�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( stdContext "context" stderrors "errors" "fmt" "net/http" "strings" "sync" "github.com/go-openapi/analysis" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag/typeutils" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/logger" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/runtime/security" "github.com/go-openapi/runtime/server-middleware/docui" "github.com/go-openapi/runtime/server-middleware/mediatype" "github.com/go-openapi/runtime/server-middleware/negotiate" ) // Debug when true turns on verbose logging. var Debug = logger.DebugEnabled() // Logger is the standard library logger used for printing debug messages. var Logger logger.Logger = logger.StandardLogger{} func debugLogfFunc(lg logger.Logger) func(string, ...any) { if logger.DebugEnabled() { if lg == nil { return Logger.Debugf } return lg.Debugf } // muted logger return func(_ string, _ ...any) {} } // A Builder can create middlewares. type Builder func(http.Handler) http.Handler // PassthroughBuilder returns the handler, aka the builder identity function. func PassthroughBuilder(handler http.Handler) http.Handler { return handler } // RequestBinder is an interface for types to implement // when they want to be able to bind from a request. type RequestBinder interface { BindRequest(*http.Request, *MatchedRoute) error } // Responder is an interface for types to implement // when they want to be considered for writing HTTP responses. type Responder interface { WriteResponse(http.ResponseWriter, runtime.Producer) } // ResponderFunc wraps a func as a Responder interface. type ResponderFunc func(http.ResponseWriter, runtime.Producer) // WriteResponse writes to the response. func (fn ResponderFunc) WriteResponse(rw http.ResponseWriter, pr runtime.Producer) { fn(rw, pr) } // Context is a type safe wrapper around an [untyped] request context // used throughout to store request context with the standard context attached // to the [http.Request]. type Context struct { spec *loads.Document analyzer *analysis.Spec api RoutableAPI router Router debugLogf func(string, ...any) // a logging function to debug context and all components using it ignoreParameters bool // see SetIgnoreParameters / WithIgnoreParameters matchSuffix bool // see SetMatchSuffix / WithMatchSuffix } // NewRoutableContext creates a new context for a routable API. // // If a nil Router is provided, the [DefaultRouter] ([denco]-based) will be used. func NewRoutableContext(spec *loads.Document, routableAPI RoutableAPI, routes Router) *Context { var an *analysis.Spec if spec != nil { an = analysis.New(spec.Spec()) } return NewRoutableContextWithAnalyzedSpec(spec, an, routableAPI, routes) } // NewRoutableContextWithAnalyzedSpec is like [NewRoutableContext] but takes as input an already analysed spec. // // If a nil Router is provided, the [DefaultRouter] ([denco]-based) will be used. func NewRoutableContextWithAnalyzedSpec(spec *loads.Document, an *analysis.Spec, routableAPI RoutableAPI, routes Router) *Context { // Either there are no spec doc and analysis, or both of them. if (spec != nil || an != nil) && (spec == nil || an == nil) { panic(fmt.Errorf("%d: %s", http.StatusInternalServerError, "routable context requires either both spec doc and analysis, or none of them")) } return &Context{ spec: spec, api: routableAPI, analyzer: an, router: routes, debugLogf: debugLogfFunc(nil), } } // NewContext creates a new context wrapper. // // If a nil Router is provided, the [DefaultRouter] ([denco]-based) will be used. func NewContext(spec *loads.Document, api *untyped.API, routes Router) *Context { var an *analysis.Spec if spec != nil { an = analysis.New(spec.Spec()) } ctx := &Context{ spec: spec, analyzer: an, router: routes, debugLogf: debugLogfFunc(nil), } ctx.api = newRoutableUntypedAPI(spec, api, ctx) return ctx } // Serve serves the specified spec with the specified api registrations as a [http.Handler]. func Serve(spec *loads.Document, api *untyped.API) http.Handler { return ServeWithBuilder(spec, api, PassthroughBuilder) } // SetIgnoreParameters toggles the legacy parameter-stripping behaviour for // Accept negotiation server-wide. When set, every internal call to // [NegotiateContentType] from this Context applies [WithIgnoreParameters]. // // Returns the receiver for fluent configuration: // // ctx := middleware.NewContext(spec, api, nil).SetIgnoreParameters(true) // // See [WithIgnoreParameters] for the rationale and an example. func (c *Context) SetIgnoreParameters(ignore bool) *Context { c.ignoreParameters = ignore return c } // SetMatchSuffix toggles RFC 6839 structured-syntax suffix tolerance // server-wide. When enabled, both Accept negotiation and codec lookup // fall back through the suffix base for the recognised suffixes // (+json, +xml, +yaml) — so an operation declaring // consumes: [application/json] also accepts request bodies sent with // Content-Type: application/vnd.api+json (or any other +json variant). // // Default: strict (false). Use only when interoperating with clients // that do not strictly abide by the spec. // // Returns the receiver for fluent configuration: // // ctx := middleware.NewContext(spec, api, nil).SetMatchSuffix(true) // // See [negotiate.WithMatchSuffix] for the per-call form and rationale. func (c *Context) SetMatchSuffix(enable bool) *Context { c.matchSuffix = enable return c } type routableUntypedAPI struct { api *untyped.API hlock *sync.Mutex handlers map[string]map[string]http.Handler defaultConsumes string defaultProduces string } func newRoutableUntypedAPI(spec *loads.Document, api *untyped.API, context *Context) *routableUntypedAPI { var handlers map[string]map[string]http.Handler if spec == nil || api == nil { return nil } analyzer := analysis.New(spec.Spec()) for method, hls := range analyzer.Operations() { um := strings.ToUpper(method) for path, op := range hls { schemes := analyzer.SecurityRequirementsFor(op) oh, ok := api.OperationHandlerFor(method, path) if !ok { continue } if handlers == nil { handlers = make(map[string]map[string]http.Handler) } if b, ok := handlers[um]; !ok || b == nil { handlers[um] = make(map[string]http.Handler) } var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // lookup route info in the context route, rCtx, _ := context.RouteInfo(r) if rCtx != nil { r = rCtx } // bind and validate the request using reflection var bound any var validation error bound, r, validation = context.BindAndValidate(r, route) if validation != nil { context.Respond(w, r, route.Produces, route, validation) return } // actually handle the request result, err := oh.Handle(bound) if err != nil { // respond with failure context.Respond(w, r, route.Produces, route, err) return } // respond with success context.Respond(w, r, route.Produces, route, result) }) if len(schemes) > 0 { handler = newSecureAPI(context, handler) } handlers[um][path] = handler } } return &routableUntypedAPI{ api: api, hlock: new(sync.Mutex), handlers: handlers, defaultProduces: api.DefaultProduces, defaultConsumes: api.DefaultConsumes, } } func (r *routableUntypedAPI) HandlerFor(method, path string) (http.Handler, bool) { r.hlock.Lock() paths, ok := r.handlers[strings.ToUpper(method)] if !ok { r.hlock.Unlock() return nil, false } handler, ok := paths[path] r.hlock.Unlock() return handler, ok } func (r *routableUntypedAPI) ServeErrorFor(_ string) func(http.ResponseWriter, *http.Request, error) { return r.api.ServeError } func (r *routableUntypedAPI) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer { return r.api.ConsumersFor(mediaTypes) } func (r *routableUntypedAPI) ProducersFor(mediaTypes []string) map[string]runtime.Producer { return r.api.ProducersFor(mediaTypes) } func (r *routableUntypedAPI) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator { return r.api.AuthenticatorsFor(schemes) } func (r *routableUntypedAPI) Authorizer() runtime.Authorizer { return r.api.Authorizer() } func (r *routableUntypedAPI) Formats() strfmt.Registry { return r.api.Formats() } func (r *routableUntypedAPI) DefaultProduces() string { return r.defaultProduces } func (r *routableUntypedAPI) DefaultConsumes() string { return r.defaultConsumes } // ServeWithBuilder serves the specified spec with the specified api registrations as a [http.Handler] that is decorated // by the Builder. func ServeWithBuilder(spec *loads.Document, api *untyped.API, builder Builder) http.Handler { context := NewContext(spec, api, nil) return context.APIHandler(builder) } type contextKey int8 const ( _ contextKey = iota ctxContentType ctxResponseFormat ctxMatchedRoute ctxBoundParams ctxSecurityPrincipal ctxSecurityScopes ) // MatchedRouteFrom request context value. func MatchedRouteFrom(req *http.Request) *MatchedRoute { mr := req.Context().Value(ctxMatchedRoute) if mr == nil { return nil } if res, ok := mr.(*MatchedRoute); ok { return res } return nil } // SecurityPrincipalFrom request context value. func SecurityPrincipalFrom(req *http.Request) any { return req.Context().Value(ctxSecurityPrincipal) } // SecurityScopesFrom request context value. func SecurityScopesFrom(req *http.Request) []string { rs := req.Context().Value(ctxSecurityScopes) if res, ok := rs.([]string); ok { return res } return nil } type contentTypeValue struct { MediaType string Charset string } // BasePath returns the base path for this API. func (c *Context) BasePath() string { if c.spec == nil { return "" } return c.spec.BasePath() } // SetLogger allows for injecting a logger to catch debug entries. // // The logger is enabled in DEBUG mode only. func (c *Context) SetLogger(lg logger.Logger) { c.debugLogf = debugLogfFunc(lg) } // RequiredProduces returns the accepted content types for responses. func (c *Context) RequiredProduces() []string { return c.analyzer.RequiredProduces() } // BindValidRequest binds a params object to a request but only when the request is valid // if the request is not valid an error will be returned. func (c *Context) BindValidRequest(request *http.Request, route *MatchedRoute, binder RequestBinder) error { var requestContentType string // check and validate content type, select consumer if runtime.HasBody(request) { ct, cons, err := c.bindRequestBody(request, route) if err != nil { return errors.CompositeValidationError(err) } // happy path requestContentType = ct route.Consumer = cons } // check and validate the response format // if the route does not provide Produces and a default contentType could not be identified // based on a body, typical for GET and DELETE requests, then default contentType to. if len(route.Produces) == 0 && requestContentType == "" { requestContentType = "*/*" } str := negotiate.ContentType(request, route.Produces, requestContentType, c.negotiateOpts()...) if str == "" { return errors.CompositeValidationError( errors.InvalidResponseFormat(request.Header.Get(runtime.HeaderAccept), route.Produces), ) } if binder == nil { return nil } // now bind the request with the provided binder // it's assumed the binder will also validate the request and return an error if the // request is invalid return binder.BindRequest(request, route) } // ContentType gets the parsed value of a content type // Returns the media type, its charset and a shallow copy of the request // when its context doesn't contain the content type value, otherwise it returns // the same request // Returns the error that [runtime.ContentType] may returns. func (c *Context) ContentType(request *http.Request) (string, string, *http.Request, error) { var rCtx = request.Context() if v, ok := rCtx.Value(ctxContentType).(*contentTypeValue); ok { return v.MediaType, v.Charset, request, nil } mt, cs, err := runtime.ContentType(request.Header) if err != nil { return "", "", nil, err } rCtx = stdContext.WithValue(rCtx, ctxContentType, &contentTypeValue{mt, cs}) return mt, cs, request.WithContext(rCtx), nil } // LookupRoute looks a route up and returns true when it is found. func (c *Context) LookupRoute(request *http.Request) (*MatchedRoute, bool) { if route, ok := c.router.Lookup(request.Method, request.URL.EscapedPath()); ok { return route, ok } return nil, false } // RouteInfo tries to match a route for this request // Returns the matched route, a shallow copy of the request if its context // contains the matched router, otherwise the same request, and a bool to // indicate if it the request matches one of the routes, if it doesn't // then it returns false and nil for the other two return values. func (c *Context) RouteInfo(request *http.Request) (*MatchedRoute, *http.Request, bool) { var rCtx = request.Context() if v, ok := rCtx.Value(ctxMatchedRoute).(*MatchedRoute); ok { return v, request, ok } if route, ok := c.LookupRoute(request); ok { rCtx = stdContext.WithValue(rCtx, ctxMatchedRoute, route) return route, request.WithContext(rCtx), ok } return nil, nil, false } // ResponseFormat negotiates the response content type // Returns the response format and a shallow copy of the request if its context // doesn't contain the response format, otherwise the same request. func (c *Context) ResponseFormat(r *http.Request, offers []string) (string, *http.Request) { var rCtx = r.Context() if v, ok := rCtx.Value(ctxResponseFormat).(string); ok { c.debugLogf("[%s %s] found response format %q in context", r.Method, r.URL.Path, v) return v, r } format := negotiate.ContentType(r, offers, "", c.negotiateOpts()...) if format != "" { c.debugLogf("[%s %s] set response format %q in context", r.Method, r.URL.Path, format) r = r.WithContext(stdContext.WithValue(rCtx, ctxResponseFormat, format)) } c.debugLogf("[%s %s] negotiated response format %q", r.Method, r.URL.Path, format) return format, r } // AllowedMethods gets the allowed methods for the path of this request. func (c *Context) AllowedMethods(request *http.Request) []string { return c.router.OtherMethods(request.Method, request.URL.EscapedPath()) } // ResetAuth removes the current principal from the request context. func (c *Context) ResetAuth(request *http.Request) *http.Request { rctx := request.Context() rctx = stdContext.WithValue(rctx, ctxSecurityPrincipal, nil) rctx = stdContext.WithValue(rctx, ctxSecurityScopes, nil) return request.WithContext(rctx) } // Authorize authorizes the request. // // Returns the principal object and a shallow copy of the request when its // context doesn't contain the principal, otherwise the same request or an error // (the last) if one of the authenticators returns one or an Unauthenticated error. func (c *Context) Authorize(request *http.Request, route *MatchedRoute) (any, *http.Request, error) { if route == nil || !route.HasAuth() { return nil, nil, nil } var rCtx = request.Context() if v := rCtx.Value(ctxSecurityPrincipal); v != nil { return v, request, nil } applies, usr, err := route.Authenticators.Authenticate(request, route) if !applies || err != nil || !route.Authenticators.AllowsAnonymous() && typeutils.IsZero(usr) { if err != nil { return nil, nil, err } return nil, nil, errors.Unauthenticated("invalid credentials") } if route.Authorizer != nil { if err := route.Authorizer.Authorize(request, usr); err != nil { var apiError errors.Error if stderrors.As(err, &apiError) { return nil, nil, err } return nil, nil, errors.New(http.StatusForbidden, "%v", err) } } rCtx = request.Context() rCtx = stdContext.WithValue(rCtx, ctxSecurityPrincipal, usr) rCtx = stdContext.WithValue(rCtx, ctxSecurityScopes, route.Authenticator.AllScopes()) return usr, request.WithContext(rCtx), nil } // BindAndValidate binds and validates the request // Returns the validation map and a shallow copy of the request when its context // doesn't contain the validation, otherwise it returns the same request or an // CompositeValidationError error. func (c *Context) BindAndValidate(request *http.Request, matched *MatchedRoute) (any, *http.Request, error) { var rCtx = request.Context() if v, ok := rCtx.Value(ctxBoundParams).(*validation); ok { c.debugLogf("got cached validation (valid: %t)", len(v.result) == 0) if len(v.result) > 0 { return v.bound, request, errors.CompositeValidationError(v.result...) } return v.bound, request, nil } result := validateRequest(c, request, matched) rCtx = stdContext.WithValue(rCtx, ctxBoundParams, result) request = request.WithContext(rCtx) if len(result.result) > 0 { return result.bound, request, errors.CompositeValidationError(result.result...) } c.debugLogf("no validation errors found") return result.bound, request, nil } // NotFound the default not found responder for when no route has been matched yet. func (c *Context) NotFound(rw http.ResponseWriter, r *http.Request) { c.Respond(rw, r, []string{c.api.DefaultProduces()}, nil, errors.NotFound("not found")) } // Respond renders the response after doing some content negotiation. func (c *Context) Respond(rw http.ResponseWriter, r *http.Request, produces []string, route *MatchedRoute, data any) { c.debugLogf("responding to %s %s with produces: %v", r.Method, r.URL.Path, produces) offers := c.buildOffers(produces) var format string format, r = c.ResponseFormat(r, offers) rw.Header().Set(runtime.HeaderContentType, format) if resp, ok := data.(Responder); ok { c.respondWithResponder(rw, r, route, resp, format) return } if err, ok := data.(error); ok { c.respondWithError(rw, r, produces, route, err, format) return } if route == nil || route.Operation == nil { c.respondWithoutCode(rw, r, data, format, offers) return } if _, code, ok := route.Operation.SuccessResponse(); ok { c.respondWithCode(rw, r, route, code, data, format) return } c.api.ServeErrorFor(route.Operation.ID)(rw, r, fmt.Errorf("%d: %s", http.StatusInternalServerError, "can't produce response")) } // APIHandlerSwaggerUI returns a handler to serve the API. // // This handler includes a swagger spec, router and the contract defined in the swagger spec. // // A spec UI ([docui.SwaggerUI]) is served at {API base path}/docs and the spec document at /swagger.json // (these can be modified with combined [UIOption]). // // Deprecated: use [Context.APIHandlerWithUI] with [docui.SwaggerUI] middleware instead. func (c *Context) APIHandlerSwaggerUI(builder Builder, opts ...UIOption) http.Handler { return c.APIHandlerWithUI(builder, docui.UseSwaggerUI, c.uiOptionsForHandler(opts)...) } // APIHandlerRapiDoc returns a handler to serve the API. // // This handler includes a swagger spec, router and the contract defined in the swagger spec. // // A spec UI ([docui.RapiDoc]) is served at {API base path}/docs and the spec document at /swagger.json // (these can be modified with combined [UIOption]). // // Deprecated: use [Context.APIHandlerWithUI] with [docui.UseRapiDoc] middleware instead. func (c *Context) APIHandlerRapiDoc(builder Builder, opts ...UIOption) http.Handler { return c.APIHandlerWithUI(builder, docui.UseRapiDoc, c.uiOptionsForHandler(opts)...) } // APIHandler returns a handler to serve the API. // // This handler includes a swagger spec, router and the contract defined in the swagger spec. // // A spec UI ([docui.Redoc]) is served at {API base path}/docs and the spec document at /swagger.json // (these can be modified with combined [UIOption]). // // Notice that you may use [Context.APIHandlerWithUI] to use an alternate UI-serving middleware. func (c *Context) APIHandler(builder Builder, opts ...UIOption) http.Handler { return c.APIHandlerWithUI(builder, docui.UseRedoc, c.uiOptionsForHandler(opts)...) } // APIHandlerWithUI returns a handler to serve the API with a swagger spec and a UI. // // This handler includes a swagger spec, router and the contract defined in the swagger spec. // // A spec UI is served at {API base path}/docs and the spec document at /swagger.json // (these can be modified with combined [UIOption]). // // Notice that any function that accepts the [docui.Option] set and returns a valid middleware may be injected here. // // [Context.APIHandlerWithUI] extends [Context.APIHandler], and supersedes [Context.APIHandlerRapiDoc] and [Context.APIHandlerSwaggerUI]. func (c *Context) APIHandlerWithUI(builder Builder, uiMiddleware docui.UIMiddleware, opts ...docui.Option) http.Handler { b := builder if b == nil { b = PassthroughBuilder } // the UI titles defaults to the title in the spec const extraOptions = 2 prepend := make([]docui.Option, 0, len(opts)+extraOptions) var title string sp := c.spec.Spec() if sp != nil && sp.Info != nil && sp.Info.Title != "" { title = sp.Info.Title } if title != "" { prepend = append(prepend, docui.WithUITitle(title)) } prepend = append(prepend, docui.WithUIBasePath(c.BasePath())) prepend = append(prepend, opts...) // aligns spec serve path with UI setting to fetch spec document. return docui.UseSpec(c.spec.Raw(), docui.WithSpecPathFromOptions(prepend...))( uiMiddleware(prepend...)( c.RoutesHandler(b), ), ) } // RoutesHandler returns a handler to serve the API, just the routes and the contract defined in the swagger spec. func (c *Context) RoutesHandler(builder Builder) http.Handler { b := builder if b == nil { b = PassthroughBuilder } return NewRouter(c, b(NewOperationExecutor(c))) } func (c *Context) bindRequestBody(request *http.Request, route *MatchedRoute) (string, runtime.Consumer, error) { ct, _, err := runtime.ContentType(request.Header) if err != nil { return "", nil, err } c.debugLogf("validating content type for %q against [%s]", ct, strings.Join(route.Consumes, ", ")) if err := validateContentType(route.Consumes, ct); err != nil { return "", nil, err } cons, ok := mediatype.Lookup(route.Consumers, ct, c.matchOpts()...) if !ok { return "", nil, errors.New(http.StatusInternalServerError, "no consumer registered for %s", ct) } return ct, cons, nil } func (c *Context) respondWithResponder(rw http.ResponseWriter, r *http.Request, route *MatchedRoute, resp Responder, format string) { _ = r producers := route.Producers // producers contains keys with normalized format, if a format has MIME type parameter such as `text/plain; charset=utf-8` // then you must provide `text/plain` to get the correct producer. HOWEVER, format here is not normalized. prod, ok := producers[normalizeOffer(format)] if !ok { prods := c.api.ProducersFor(normalizeOffers([]string{c.api.DefaultProduces()})) pr, ok := prods[c.api.DefaultProduces()] if !ok { panic(fmt.Errorf("%d: %s", http.StatusInternalServerError, cantFindProducer(format))) } prod = pr } resp.WriteResponse(rw, prod) } func (c *Context) respondWithError(rw http.ResponseWriter, r *http.Request, produces []string, route *MatchedRoute, err error, format string) { _ = produces if format == "" { rw.Header().Set(runtime.HeaderContentType, runtime.JSONMime) } if realm := security.FailedBasicAuth(r); realm != "" { rw.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", realm)) } if route == nil || route.Operation == nil { c.api.ServeErrorFor("")(rw, r, err) return } c.api.ServeErrorFor(route.Operation.ID)(rw, r, err) } func (c *Context) respondWithoutCode(rw http.ResponseWriter, r *http.Request, data any, format string, offers []string) { rw.WriteHeader(http.StatusOK) if r.Method == http.MethodHead { return } producers := c.api.ProducersFor(normalizeOffers(offers)) prod, ok := producers[format] if !ok { panic(fmt.Errorf("%d: %s", http.StatusInternalServerError, cantFindProducer(format))) } if err := prod.Produce(rw, data); err != nil { panic(err) // let the recovery middleware deal with this } } func (c *Context) buildOffers(produces []string) []string { offers := make([]string, 0, len(produces)+1) for _, mt := range produces { if mt != c.api.DefaultProduces() { offers = append(offers, mt) } } // the default producer is last so more specific producers take precedence offers = append(offers, c.api.DefaultProduces()) c.debugLogf("offers: %v", offers) return offers } func (c *Context) respondWithCode(rw http.ResponseWriter, r *http.Request, route *MatchedRoute, code int, data any, format string) { rw.WriteHeader(code) if code == http.StatusNoContent || r.Method == http.MethodHead { return } producers := route.Producers prod, ok := producers[format] if !ok { if !ok { prods := c.api.ProducersFor(normalizeOffers([]string{c.api.DefaultProduces()})) pr, ok := prods[c.api.DefaultProduces()] if !ok { panic(fmt.Errorf("%d: %s", http.StatusInternalServerError, cantFindProducer(format))) } prod = pr } } if err := prod.Produce(rw, data); err != nil { panic(err) // let the recovery middleware deal with this } } // uiOptionsForHandler bridges the deprecated [UIOption] set to the new [docui.Option] set. func (c Context) uiOptionsForHandler(opts []UIOption) []docui.Option { uiOpts := uiOptionsWithDefaults(opts) return uiOpts.toFuncOptions() } func (c *Context) negotiateOpts() []negotiate.Option { var opts []negotiate.Option if c.ignoreParameters { opts = append(opts, negotiate.WithIgnoreParameters(true)) } if c.matchSuffix { opts = append(opts, negotiate.WithMatchSuffix(true)) } return opts } // matchOpts builds the mediatype.MatchOption slice that the // codec-lookup and Content-Type validation paths apply server-wide. // Mirrors negotiateOpts but at the mediatype level (without going // through the negotiate.Option wrapper). func (c *Context) matchOpts() []mediatype.MatchOption { if !c.matchSuffix { return nil } return []mediatype.MatchOption{mediatype.AllowSuffix()} } func cantFindProducer(format string) string { return "can't find a producer for " + format } go-openapi-runtime-decad8f/middleware/context_test.go�����������������������������������������������0000664�0000000�0000000�00000061327�15202323100�0023255�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( stdcontext "context" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" apierrors "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/loads/fmts" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/internal/testing/petstore" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) type stubBindRequester struct { } func (s *stubBindRequester) BindRequest(*http.Request, *MatchedRoute) error { return nil } type stubOperationHandler struct { } func (s *stubOperationHandler) ParameterModel() any { return nil } func (s *stubOperationHandler) Handle(_ any) (any, error) { return map[string]any{}, nil } func init() { loads.AddLoader(fmts.YAMLMatcher, fmts.YAMLDoc) } func assertAPIError(t *testing.T, wantCode int, err error) { t.Helper() require.Error(t, err) var ce *apierrors.CompositeError require.TrueT(t, errors.As(err, &ce)) require.NotEmpty(t, ce.Errors) var ae apierrors.Error require.TrueT(t, errors.As(ce.Errors[0], &ae)) assert.EqualT(t, wantCode, int(ae.Code())) } func TestContentType_Issue264(t *testing.T) { swspec, err := loads.Spec("../fixtures/bugs/264/swagger.yml") require.NoError(t, err) api := untyped.NewAPI(swspec) api.RegisterConsumer(runtime.JSONMime, runtime.JSONConsumer()) api.RegisterProducer(runtime.JSONMime, runtime.JSONProducer()) api.RegisterOperation("delete", "/key/{id}", new(stubOperationHandler)) handler := Serve(swspec, api) request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodDelete, "/key/1", nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) } func TestContentType_Issue172(t *testing.T) { swspec, err := loads.Spec("../fixtures/bugs/172/swagger.yml") require.NoError(t, err) api := untyped.NewAPI(swspec) api.RegisterConsumer("application/vnd.cia.v1+json", runtime.JSONConsumer()) api.RegisterProducer("application/vnd.cia.v1+json", runtime.JSONProducer()) api.RegisterOperation("get", "/pets", new(stubOperationHandler)) handler := Serve(swspec, api) request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/pets", nil) require.NoError(t, err) request.Header.Add("Accept", "application/json+special") recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusNotAcceptable, recorder.Code) // acceptable as defined as default by the API (not explicit in the spec) request.Header.Add("Accept", runtime.JSONMime) recorder = httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) } func TestContentType_Issue174(t *testing.T) { swspec, err := loads.Spec("../fixtures/bugs/174/swagger.yml") require.NoError(t, err) api := untyped.NewAPI(swspec) api.RegisterConsumer(runtime.JSONMime, runtime.JSONConsumer()) api.RegisterProducer(runtime.JSONMime, runtime.JSONProducer()) api.RegisterOperation("get", "/pets", new(stubOperationHandler)) handler := Serve(swspec, api) request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/pets", nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) } const ( testHost = "https://localhost:8080" // how to get the spec document? defaultSpecPath = "/swagger.json" defaultSpecURL = testHost + defaultSpecPath // how to get the UI asset? defaultUIURL = testHost + "/api/docs" ) func TestServe(t *testing.T) { spec, api := petstore.NewAPI(t) handler := Serve(spec, api) t.Run("serve spec document", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, defaultSpecURL, nil) require.NoError(t, err) request.Header.Add("Content-Type", runtime.JSONMime) request.Header.Add("Accept", runtime.JSONMime) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) }) t.Run("should not find UI there", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, testHost+"/swagger-ui", nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusNotFound, recorder.Code) }) t.Run("should find UI here", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, defaultUIURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) htmlResponse := recorder.Body.String() assert.StringContainsTf(t, htmlResponse, "<title>Swagger Petstore", "should default to the API's title") assert.StringContainsTf(t, htmlResponse, "", "should default to /swagger.json spec document") }) } func TestServeWithUIs(t *testing.T) { spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) const ( alternateSpecURL = testHost + "/specs/petstore.json" alternateSpecPath = "/specs/petstore.json" alternateUIURL = testHost + "/ui/docs" ) uiOpts := []UIOption{ WithUIBasePath("ui"), // override the base path from the spec, implies /ui WithUIPath("docs"), WithUISpecURL("/specs/petstore.json"), } t.Run("with APIHandler", func(t *testing.T) { t.Run("with defaults", func(t *testing.T) { handler := ctx.APIHandler(nil) t.Run("should find UI", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, defaultUIURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) htmlResponse := recorder.Body.String() assert.StringContainsTf(t, htmlResponse, "", alternateSpecPath)) }) t.Run("should find spec", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, alternateSpecURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) }) }) }) t.Run("with APIHandlerSwaggerUI", func(t *testing.T) { t.Run("with defaults", func(t *testing.T) { handler := ctx.APIHandlerSwaggerUI(nil) t.Run("should find UI", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, defaultUIURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) htmlResponse := recorder.Body.String() assert.StringContainsT(t, htmlResponse, fmt.Sprintf(`url: '%s',`, strings.ReplaceAll(defaultSpecPath, `/`, `\/`))) }) t.Run("should find spec", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, defaultSpecURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) }) }) t.Run("with options", func(t *testing.T) { handler := ctx.APIHandlerSwaggerUI(nil, uiOpts...) t.Run("should find UI", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, alternateUIURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) htmlResponse := recorder.Body.String() assert.StringContainsT(t, htmlResponse, fmt.Sprintf(`url: '%s',`, strings.ReplaceAll(alternateSpecPath, `/`, `\/`))) }) t.Run("should find spec", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, alternateSpecURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) }) }) }) t.Run("with APIHandlerRapiDoc", func(t *testing.T) { t.Run("with defaults", func(t *testing.T) { handler := ctx.APIHandlerRapiDoc(nil) t.Run("should find UI", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, defaultUIURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) htmlResponse := recorder.Body.String() assert.StringContainsT(t, htmlResponse, fmt.Sprintf("", defaultSpecPath)) }) t.Run("should find spec", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, defaultSpecURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) }) }) t.Run("with options", func(t *testing.T) { handler := ctx.APIHandlerRapiDoc(nil, uiOpts...) t.Run("should find UI", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, alternateUIURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) htmlResponse := recorder.Body.String() assert.StringContainsT(t, htmlResponse, fmt.Sprintf("", alternateSpecPath)) }) t.Run("should find spec", func(t *testing.T) { request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, alternateSpecURL, nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) }) }) }) } func TestContextAuthorize(t *testing.T) { spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) request, err := runtime.JSONRequest(http.MethodGet, "/api/pets", nil) require.NoError(t, err) request = request.WithContext(stdcontext.Background()) ri, reqWithCtx, ok := ctx.RouteInfo(request) assert.TrueT(t, ok) require.NotNil(t, reqWithCtx) request = reqWithCtx p, reqWithCtx, err := ctx.Authorize(request, ri) require.Error(t, err) assert.Nil(t, p) assert.Nil(t, reqWithCtx) v := request.Context().Value(ctxSecurityPrincipal) assert.Nil(t, v) request.SetBasicAuth("wrong", "wrong") p, reqWithCtx, err = ctx.Authorize(request, ri) require.Error(t, err) assert.Nil(t, p) assert.Nil(t, reqWithCtx) v = request.Context().Value(ctxSecurityPrincipal) assert.Nil(t, v) request.SetBasicAuth("admin", "admin") p, reqWithCtx, err = ctx.Authorize(request, ri) require.NoError(t, err) assert.Equal(t, "admin", p) require.NotNil(t, reqWithCtx) // Assign the new returned request to follow with the test request = reqWithCtx v, ok = request.Context().Value(ctxSecurityPrincipal).(string) assert.TrueT(t, ok) assert.Equal(t, "admin", v) // Once the request context contains the principal the authentication // isn't rechecked request.SetBasicAuth("doesn't matter", "doesn't") pp, reqCtx, rr := ctx.Authorize(request, ri) assert.Equal(t, p, pp) assert.Equal(t, err, rr) assert.Equal(t, request, reqCtx) } func TestContextAuthorize_WithAuthorizer(t *testing.T) { spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) request, err := runtime.JSONRequest(http.MethodPost, "/api/pets", nil) require.NoError(t, err) request = request.WithContext(stdcontext.Background()) ri, reqWithCtx, ok := ctx.RouteInfo(request) assert.TrueT(t, ok) require.NotNil(t, reqWithCtx) request = reqWithCtx request.SetBasicAuth("topuser", "topuser") p, reqWithCtx, err := ctx.Authorize(request, ri) assertAPIError(t, apierrors.InvalidTypeCode, err) assert.Nil(t, p) assert.Nil(t, reqWithCtx) request.SetBasicAuth("admin", "admin") p, reqWithCtx, err = ctx.Authorize(request, ri) require.NoError(t, err) assert.Equal(t, "admin", p) require.NotNil(t, reqWithCtx) request.SetBasicAuth("anyother", "anyother") p, reqWithCtx, err = ctx.Authorize(request, ri) require.Error(t, err) var ae apierrors.Error require.TrueTf(t, errors.As(err, &ae), "expected an apierrors.Error, but got %T", err) assert.EqualT(t, http.StatusForbidden, int(ae.Code())) assert.Nil(t, p) assert.Nil(t, reqWithCtx) } func TestContextNegotiateContentType(t *testing.T) { spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) request, _ := http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", nil) // request.Header.Add("Accept", "*/*") request.Header.Add("Content-Type", "text/html") v := request.Context().Value(ctxBoundParams) assert.Nil(t, v) ri, request, _ := ctx.RouteInfo(request) res := NegotiateContentType(request, ri.Produces, "text/plain") assert.EqualT(t, ri.Produces[0], res) } func TestContextBindValidRequest(t *testing.T) { spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) // invalid content-type value request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", strings.NewReader(`{"name":"dog"}`)) require.NoError(t, err) request.Header.Add("Content-Type", "/json") ri, request, _ := ctx.RouteInfo(request) assertAPIError(t, http.StatusBadRequest, ctx.BindValidRequest(request, ri, new(stubBindRequester))) // unsupported content-type value request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", strings.NewReader(`{"name":"dog"}`)) require.NoError(t, err) request.Header.Add("Content-Type", "text/html") ri, request, _ = ctx.RouteInfo(request) assertAPIError(t, http.StatusUnsupportedMediaType, ctx.BindValidRequest(request, ri, new(stubBindRequester))) // unacceptable accept value request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", nil) require.NoError(t, err) request.Header.Add("Accept", "application/vnd.cia.v1+json") request.Header.Add("Content-Type", runtime.JSONMime) ri, request, _ = ctx.RouteInfo(request) assertAPIError(t, http.StatusNotAcceptable, ctx.BindValidRequest(request, ri, new(stubBindRequester))) } func TestContextBindValidRequest_Issue174(t *testing.T) { spec, err := loads.Spec("../fixtures/bugs/174/swagger.yml") require.NoError(t, err) api := untyped.NewAPI(spec) api.RegisterConsumer(runtime.JSONMime, runtime.JSONConsumer()) api.RegisterProducer(runtime.JSONMime, runtime.JSONProducer()) api.RegisterOperation("get", "/pets", new(stubOperationHandler)) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) request, _ := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/pets", nil) ri, request, _ := ctx.RouteInfo(request) require.NoError(t, ctx.BindValidRequest(request, ri, new(stubBindRequester))) } func TestContextBindAndValidate(t *testing.T) { spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) request, _ := http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", nil) request.Header.Add("Accept", "*/*") request.Header.Add("Content-Type", "text/html") request.ContentLength = 1 v := request.Context().Value(ctxBoundParams) assert.Nil(t, v) ri, request, _ := ctx.RouteInfo(request) data, request, result := ctx.BindAndValidate(request, ri) // this requires a much more thorough test assert.NotNil(t, data) require.Error(t, result) v, ok := request.Context().Value(ctxBoundParams).(*validation) assert.TrueT(t, ok) assert.NotNil(t, v) dd, rCtx, rr := ctx.BindAndValidate(request, ri) assert.Equal(t, data, dd) assert.Equal(t, result, rr) assert.Equal(t, rCtx, request) } func TestContextRender(t *testing.T) { ct := runtime.JSONMime spec, api := petstore.NewAPI(t) assert.NotNil(t, spec) assert.NotNil(t, api) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) request, _ := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) request.Header.Set(runtime.HeaderAccept, ct) ri, request, _ := ctx.RouteInfo(request) recorder := httptest.NewRecorder() ctx.Respond(recorder, request, []string{ct}, ri, map[string]any{paramKeyName: valHello}) assert.EqualT(t, http.StatusOK, recorder.Code) assert.JSONEqT(t, "{\"name\":\"hello\"}\n", recorder.Body.String()) recorder = httptest.NewRecorder() ctx.Respond(recorder, request, []string{ct}, ri, errors.New("this went wrong")) assert.EqualT(t, http.StatusInternalServerError, recorder.Code) // recorder = httptest.NewRecorder() // assert.Panics(t, func() { ctx.Respond(recorder, request, []string{ct}, ri, map[int]interface{}{1: "hello"}) }) // Panic when route is nil and there is not a producer for the requested response format recorder = httptest.NewRecorder() request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) request.Header.Set(runtime.HeaderAccept, "text/xml") assert.Panics(t, func() { ctx.Respond(recorder, request, []string{}, nil, map[string]any{paramKeyName: valHello}) }) request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) request.Header.Set(runtime.HeaderAccept, ct) ri, request, _ = ctx.RouteInfo(request) recorder = httptest.NewRecorder() ctx.Respond(recorder, request, []string{ct}, ri, map[string]any{paramKeyName: valHello}) assert.EqualT(t, http.StatusOK, recorder.Code) assert.JSONEqT(t, "{\"name\":\"hello\"}\n", recorder.Body.String()) recorder = httptest.NewRecorder() ctx.Respond(recorder, request, []string{ct}, ri, errors.New("this went wrong")) assert.EqualT(t, http.StatusInternalServerError, recorder.Code) // recorder = httptest.NewRecorder() // assert.Panics(t, func() { ctx.Respond(recorder, request, []string{ct}, ri, map[int]interface{}{1: "hello"}) }) // recorder = httptest.NewRecorder() // request, _ = http.NewRequestWithContext(stdcontext.Background(),http.MethodGet, "/pets", nil) // assert.Panics(t, func() { ctx.Respond(recorder, request, []string{}, ri, map[string]interface{}{"name": "hello"}) }) recorder = httptest.NewRecorder() request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodDelete, "/api/pets/1", nil) require.NoError(t, err) ri, request, _ = ctx.RouteInfo(request) ctx.Respond(recorder, request, ri.Produces, ri, nil) assert.EqualT(t, http.StatusNoContent, recorder.Code) } func TestContextValidResponseFormat(t *testing.T) { const ct = runtime.JSONMime spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "http://localhost:8080", nil) require.NoError(t, err) request.Header.Set(runtime.HeaderAccept, ct) // check there's nothing there cached, ok := request.Context().Value(ctxResponseFormat).(string) assert.FalseT(t, ok) assert.Empty(t, cached) // trigger the parse mt, request := ctx.ResponseFormat(request, []string{ct}) assert.EqualT(t, ct, mt) // check it was cached cached, ok = request.Context().Value(ctxResponseFormat).(string) assert.TrueT(t, ok) assert.EqualT(t, ct, cached) // check if the cast works and fetch from cache too mt, _ = ctx.ResponseFormat(request, []string{ct}) assert.EqualT(t, ct, mt) } func TestContextInvalidResponseFormat(t *testing.T) { ct := "application/x-yaml" other := "application/sgml" spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "http://localhost:8080", nil) require.NoError(t, err) request.Header.Set(runtime.HeaderAccept, ct) // check there's nothing there cached, ok := request.Context().Value(ctxResponseFormat).(string) assert.FalseT(t, ok) assert.Empty(t, cached) // trigger the parse mt, request := ctx.ResponseFormat(request, []string{other}) assert.Empty(t, mt) // check it was cached cached, ok = request.Context().Value(ctxResponseFormat).(string) assert.FalseT(t, ok) assert.Empty(t, cached) // check if the cast works and fetch from cache too mt, rCtx := ctx.ResponseFormat(request, []string{other}) assert.Empty(t, mt) assert.Equal(t, request, rCtx) } func TestContextValidRoute(t *testing.T) { spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) // check there's nothing there cached := request.Context().Value(ctxMatchedRoute) assert.Nil(t, cached) matched, rCtx, ok := ctx.RouteInfo(request) assert.TrueT(t, ok) assert.NotNil(t, matched) assert.NotNil(t, rCtx) assert.NotEqual(t, request, rCtx) request = rCtx // check it was cached _, ok = request.Context().Value(ctxMatchedRoute).(*MatchedRoute) assert.TrueT(t, ok) matched, rCtx, ok = ctx.RouteInfo(request) assert.TrueT(t, ok) assert.NotNil(t, matched) assert.Equal(t, request, rCtx) } func TestContextInvalidRoute(t *testing.T) { spec, api := petstore.NewAPI(t) ctx := NewContext(spec, api, nil) ctx.router = DefaultRouter(spec, ctx.api) request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodDelete, "pets", nil) require.NoError(t, err) // check there's nothing there cached := request.Context().Value(ctxMatchedRoute) assert.Nil(t, cached) matched, rCtx, ok := ctx.RouteInfo(request) assert.FalseT(t, ok) assert.Nil(t, matched) assert.Nil(t, rCtx) // check it was not cached cached = request.Context().Value(ctxMatchedRoute) assert.Nil(t, cached) matched, rCtx, ok = ctx.RouteInfo(request) assert.FalseT(t, ok) assert.Nil(t, matched) assert.Nil(t, rCtx) } func TestContextValidContentType(t *testing.T) { ct := runtime.JSONMime ctx := NewContext(nil, nil, nil) request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "http://localhost:8080", nil) require.NoError(t, err) request.Header.Set(runtime.HeaderContentType, ct) // check there's nothing there cached := request.Context().Value(ctxContentType) assert.Nil(t, cached) // trigger the parse mt, _, rCtx, err := ctx.ContentType(request) require.NoError(t, err) assert.EqualT(t, ct, mt) assert.NotNil(t, rCtx) assert.NotEqual(t, request, rCtx) request = rCtx // check it was cached cached = request.Context().Value(ctxContentType) assert.NotNil(t, cached) // check if the cast works and fetch from cache too mt, _, rCtx, err = ctx.ContentType(request) require.NoError(t, err) assert.EqualT(t, ct, mt) assert.Equal(t, request, rCtx) } func TestContextInvalidContentType(t *testing.T) { ct := "application(" ctx := NewContext(nil, nil, nil) request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "http://localhost:8080", nil) require.NoError(t, err) request.Header.Set(runtime.HeaderContentType, ct) // check there's nothing there cached := request.Context().Value(ctxContentType) assert.Nil(t, cached) // trigger the parse mt, _, rCtx, err := ctx.ContentType(request) require.Error(t, err) assert.Empty(t, mt) assert.Nil(t, rCtx) // check it was not cached cached = request.Context().Value(ctxContentType) assert.Nil(t, cached) // check if the failure continues _, _, rCtx, err = ctx.ContentType(request) require.Error(t, err) assert.Nil(t, rCtx) } go-openapi-runtime-decad8f/middleware/debug_test.go000066400000000000000000000066561520232310000226630ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "bytes" stdcontext "context" "log" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/runtime/internal/testing/petstore" "github.com/go-openapi/runtime/logger" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) type customLogger struct { logger.StandardLogger lg *log.Logger } func (l customLogger) Debugf(format string, args ...any) { l.lg.Printf(format, args...) } func TestDebugMode(t *testing.T) { t.Run("with normal mode", func(t *testing.T) { t.Setenv("DEBUG", "") logFunc := debugLogfFunc(nil) require.NotNil(t, logFunc) }) t.Run("with debug mode", func(t *testing.T) { t.Setenv("DEBUG", "true") t.Run("debugLogFunc with nil logger yields standard logger", func(t *testing.T) { logFunc := debugLogfFunc(nil) require.NotNil(t, logFunc) }) t.Run("debugLogFunc with custom logger", func(t *testing.T) { var capture bytes.Buffer logger := customLogger{lg: log.New(&capture, "test", log.Lshortfile)} logFunc := debugLogfFunc(logger) require.NotNil(t, logFunc) logFunc("debug") assert.NotEmpty(t, capture.String()) }) }) } func TestDebugRouterOptions(t *testing.T) { t.Run("with normal mode", func(t *testing.T) { t.Setenv("DEBUG", "") t.Run("should capture debug from context & router", func(t *testing.T) { var capture bytes.Buffer logger := customLogger{lg: log.New(&capture, "test", log.Lshortfile)} t.Run("run some activiy", doCheckWithContext(logger)) assert.Empty(t, capture.String()) }) t.Run("should capture debug from standalone DefaultRouter", func(t *testing.T) { var capture bytes.Buffer logger := customLogger{lg: log.New(&capture, "test", log.Lshortfile)} t.Run("run some activiy", doCheckWithDefaultRouter(logger)) assert.Empty(t, capture.String()) }) }) t.Run("with debug mode", func(t *testing.T) { t.Setenv("DEBUG", "1") t.Run("should capture debug from context & router", func(t *testing.T) { var capture bytes.Buffer logger := customLogger{lg: log.New(&capture, "test", log.Lshortfile)} t.Run("run some activiy", doCheckWithContext(logger)) assert.NotEmpty(t, capture.String()) }) t.Run("should capture debug from standalone DefaultRouter", func(t *testing.T) { var capture bytes.Buffer logger := customLogger{lg: log.New(&capture, "test", log.Lshortfile)} t.Run("run some activiy", doCheckWithDefaultRouter(logger)) assert.NotEmpty(t, capture.String()) }) }) } func doCheckWithContext(logger logger.Logger) func(*testing.T) { return func(t *testing.T) { spec, api := petstore.NewAPI(t) context := NewContext(spec, api, nil) context.SetLogger(logger) mw := NewRouter(context, http.HandlerFunc(terminator)) recorder := httptest.NewRecorder() request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) } } func doCheckWithDefaultRouter(lg logger.Logger) func(*testing.T) { return func(t *testing.T) { spec, api := petstore.NewAPI(t) context := NewContext(spec, api, nil) context.SetLogger(lg) router := DefaultRouter( spec, newRoutableUntypedAPI(spec, api, new(Context)), WithDefaultRouterLogger(lg)) _ = router.OtherMethods("post", "/api/pets/{id}") } } go-openapi-runtime-decad8f/middleware/denco/000077500000000000000000000000001520232310000212625ustar00rootroot00000000000000go-openapi-runtime-decad8f/middleware/denco/LICENSE000066400000000000000000000020621520232310000222670ustar00rootroot00000000000000Copyright (c) 2014 Naoya Inada Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-openapi-runtime-decad8f/middleware/denco/README.md000066400000000000000000000111641520232310000225440ustar00rootroot00000000000000# Denco [![Build Status](https://travis-ci.org/naoina/denco.png?branch=master)](https://travis-ci.org/naoina/denco) The fast and flexible HTTP request router for [Go](http://golang.org). Denco is based on Double-Array implementation of [Kocha-urlrouter](https://github.com/naoina/kocha-urlrouter). However, Denco is optimized and some features added. ## Features * Fast (See [go-http-routing-benchmark](https://github.com/naoina/go-http-routing-benchmark)) *[URL patterns](#url-patterns) (`/foo/:bar` and `/foo/*wildcard`) * Small (but enough) URL router API * HTTP request multiplexer like `http.ServeMux` ## Installation go get -u github.com/go-openapi/runtime/middleware/denco ## Using as HTTP request multiplexer ```go package main import ( "fmt" "log" "net/http" "github.com/go-openapi/runtime/middleware/denco" ) func Index(w http.ResponseWriter, r *http.Request, params denco.Params) { fmt.Fprintf(w, "Welcome to Denco!\n") } func User(w http.ResponseWriter, r *http.Request, params denco.Params) { fmt.Fprintf(w, "Hello %s!\n", params.Get("name")) } func main() { mux := denco.NewMux() handler, err := mux.Build([]denco.Handler{ mux.GET("/", Index), mux.GET("/user/:name", User), mux.POST("/user/:name", User), }) if err != nil { panic(err) } log.Fatal(http.ListenAndServe(":8080", handler)) } ``` ## Using as URL router ```go package main import ( "fmt" "github.com/go-openapi/runtime/middleware/denco" ) type route struct { name string } func main() { router := denco.New() router.Build([]denco.Record{ {"/", &route{"root"}}, {"/user/:id", &route{"user"}}, {"/user/:name/:id", &route{"username"}}, {"/static/*filepath", &route{"static"}}, }) data, params, found := router.Lookup("/") // print `&main.route{name:"root"}, denco.Params(nil), true`. fmt.Printf("%#v, %#v, %#v\n", data, params, found) data, params, found = router.Lookup("/user/hoge") // print `&main.route{name:"user"}, denco.Params{denco.Param{Name:"id", Value:"hoge"}}, true`. fmt.Printf("%#v, %#v, %#v\n", data, params, found) data, params, found = router.Lookup("/user/hoge/7") // print `&main.route{name:"username"}, denco.Params{denco.Param{Name:"name", Value:"hoge"}, denco.Param{Name:"id", Value:"7"}}, true`. fmt.Printf("%#v, %#v, %#v\n", data, params, found) data, params, found = router.Lookup("/static/path/to/file") // print `&main.route{name:"static"}, denco.Params{denco.Param{Name:"filepath", Value:"path/to/file"}}, true`. fmt.Printf("%#v, %#v, %#v\n", data, params, found) } ``` See [Godoc](http://godoc.org/github.com/go-openapi/runtime/middleware/denco) for more details. ## Getting the value of path parameter You can get the value of path parameter by 2 ways. 1. Using [`denco.Params.Get`](http://godoc.org/github.com/go-openapi/runtime/middleware/denco#Params.Get) method 2. Find by loop ```go package main import ( "fmt" "github.com/go-openapi/runtime/middleware/denco" ) func main() { router := denco.New() if err := router.Build([]denco.Record{ {"/user/:name/:id", "route1"}, }); err != nil { panic(err) } // 1. Using denco.Params.Get method. _, params, _ := router.Lookup("/user/alice/1") name := params.Get("name") if name != "" { fmt.Printf("Hello %s.\n", name) // prints "Hello alice.". } // 2. Find by loop. for _, param := range params { if param.Name == "name" { fmt.Printf("Hello %s.\n", name) // prints "Hello alice.". } } } ``` ## URL patterns Denco's route matching strategy is "most nearly matching". When routes `/:name` and `/alice` have been built, URI `/alice` matches the route `/alice`, not `/:name`. Because URI `/alice` is more match with the route `/alice` than `/:name`. For more example, when routes below have been built: ``` /user/alice /user/:name /user/:name/:id /user/alice/:id /user/:id/bob ``` Routes matching are: ``` /user/alice => "/user/alice" (no match with "/user/:name") /user/bob => "/user/:name" /user/naoina/1 => "/user/:name/1" /user/alice/1 => "/user/alice/:id" (no match with "/user/:name/:id") /user/1/bob => "/user/:id/bob" (no match with "/user/:name/:id") /user/alice/bob => "/user/alice/:id" (no match with "/user/:name/:id" and "/user/:id/bob") ``` ## Limitation Denco has some limitations below. * Number of param records (such as `/:name`) must be less than 2^22 * Number of elements of internal slice must be less than 2^22 ## Benchmarks cd $GOPATH/github.com/go-openapi/runtime/middleware/denco go test -bench . -benchmem ## License Denco is licensed under the MIT License. go-openapi-runtime-decad8f/middleware/denco/consts_test.go000066400000000000000000000240071520232310000241640ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2014 Naoya Inada // SPDX-License-Identifier: MIT package denco_test // Test-only constants extracted to satisfy the goconst linter. // They are shared across router_test.go, server_test.go and util_test.go. const ( testRoute0 = "testroute0" testRoute1 = "testroute1" testRoute2 = "testroute2" testRoute3 = "testroute3" ) const notFoundBody = "404 page not found\n" const ( pathPathToRoute = "/path/to/route" pathUserAlice = "/user/alice" pathUserID = "/user/:id" pathUserIDGroup = "/user/:id/:group" pathUserIDPostCID = "/user/:id/post/:cid" ) const ( pathActivities = "/activities" pathActivitiesActivityID = "/activities/:activityId" pathActivitiesComments = "/activities/:activityId/comments" pathActivitiesPeople = "/activities/:activityId/people/:collection" pathAppsTokens = "/applications/:client_id/tokens/:access_token" pathAuthorizations = "/authorizations" pathAuthorizationsID = "/authorizations/:id" pathCommentsCommentID = "/comments/:commentId" pathEmojis = "/emojis" pathEvents = "/events" pathFeeds = "/feeds" pathGists = "/gists" pathGistsID = "/gists/:id" pathGistsIDStar = "/gists/:id/star" pathGitignoreTemplate = "/gitignore/templates/:name" pathGitignoreTemplates = "/gitignore/templates" pathIssues = "/issues" pathLegacyIssuesSearch = "/legacy/issues/search/:owner/:repository/:state/:keyword" pathLegacyReposSearch = "/legacy/repos/search/:keyword" pathLegacyUserEmail = "/legacy/user/email/:email" pathLegacyUserSearch = "/legacy/user/search/:keyword" pathMeta = "/meta" pathNetworksEvents = "/networks/:owner/:repo/events" pathNotifications = "/notifications" pathNotificationThreadSub = "/notifications/threads/:id/subscription" pathNotificationThreads = "/notifications/threads/:id" pathOrgsEvents = "/orgs/:org/events" pathOrgsIssues = "/orgs/:org/issues" pathOrgsMember = "/orgs/:org/members/:user" pathOrgsMembers = "/orgs/:org/members" pathOrgsOrg = "/orgs/:org" pathOrgsPublicMember = "/orgs/:org/public_members/:user" pathOrgsPublicMembers = "/orgs/:org/public_members" pathOrgsRepos = "/orgs/:org/repos" pathOrgsTeams = "/orgs/:org/teams" pathPeople = "/people" pathPeopleActivities = "/people/:userId/activities/:collection" pathPeopleMoments = "/people/:userId/moments/:collection" pathPeopleOpenIDConnect = "/people/:userId/openIdConnect" pathPeoplePeople = "/people/:userId/people/:collection" pathPeopleUserID = "/people/:userId" pathRateLimit = "/rate_limit" pathRepoOwnerRepo = "/repos/:owner/:repo" pathRepositories = "/repositories" pathReposAssignee = "/repos/:owner/:repo/assignees/:assignee" pathReposAssignees = "/repos/:owner/:repo/assignees" pathReposBranch = "/repos/:owner/:repo/branches/:branch" pathReposBranches = "/repos/:owner/:repo/branches" pathReposCollaborator = "/repos/:owner/:repo/collaborators/:user" pathReposCollaborators = "/repos/:owner/:repo/collaborators" pathReposComment = "/repos/:owner/:repo/comments/:id" pathReposComments = "/repos/:owner/:repo/comments" pathReposCommit = "/repos/:owner/:repo/commits/:sha" pathReposCommits = "/repos/:owner/:repo/commits" pathReposCommitsSHAComments = "/repos/:owner/:repo/commits/:sha/comments" pathReposContributors = "/repos/:owner/:repo/contributors" pathReposDownload = "/repos/:owner/:repo/downloads/:id" pathReposDownloads = "/repos/:owner/:repo/downloads" pathReposEvents = "/repos/:owner/:repo/events" pathReposForks = "/repos/:owner/:repo/forks" pathReposGitBlobs = "/repos/:owner/:repo/git/blobs/:sha" pathReposGitCommits = "/repos/:owner/:repo/git/commits/:sha" pathReposGitRefs = "/repos/:owner/:repo/git/refs" pathReposGitTags = "/repos/:owner/:repo/git/tags/:sha" pathReposGitTrees = "/repos/:owner/:repo/git/trees/:sha" pathReposHook = "/repos/:owner/:repo/hooks/:id" pathReposHooks = "/repos/:owner/:repo/hooks" pathReposIssue = "/repos/:owner/:repo/issues/:number" pathReposIssueComments = "/repos/:owner/:repo/issues/:number/comments" pathReposIssueEvents = "/repos/:owner/:repo/issues/:number/events" pathReposIssueLabels = "/repos/:owner/:repo/issues/:number/labels" pathReposIssues = "/repos/:owner/:repo/issues" pathReposKey = "/repos/:owner/:repo/keys/:id" pathReposKeys = "/repos/:owner/:repo/keys" pathReposLabel = "/repos/:owner/:repo/labels/:name" pathReposLabels = "/repos/:owner/:repo/labels" pathReposLanguages = "/repos/:owner/:repo/languages" pathReposMilestone = "/repos/:owner/:repo/milestones/:number" pathReposMilestoneLabels = "/repos/:owner/:repo/milestones/:number/labels" pathReposMilestones = "/repos/:owner/:repo/milestones" pathReposNotifications = "/repos/:owner/:repo/notifications" pathReposPull = "/repos/:owner/:repo/pulls/:number" pathReposPullComments = "/repos/:owner/:repo/pulls/:number/comments" pathReposPullCommits = "/repos/:owner/:repo/pulls/:number/commits" pathReposPullFiles = "/repos/:owner/:repo/pulls/:number/files" pathReposPullMerge = "/repos/:owner/:repo/pulls/:number/merge" pathReposPulls = "/repos/:owner/:repo/pulls" pathReposReadme = "/repos/:owner/:repo/readme" pathReposRelease = "/repos/:owner/:repo/releases/:id" pathReposReleaseAssets = "/repos/:owner/:repo/releases/:id/assets" pathReposReleases = "/repos/:owner/:repo/releases" pathReposStargazers = "/repos/:owner/:repo/stargazers" pathReposStatsCodeFrequency = "/repos/:owner/:repo/stats/code_frequency" pathReposStatsCommitActivity = "/repos/:owner/:repo/stats/commit_activity" pathReposStatsContributors = "/repos/:owner/:repo/stats/contributors" pathReposStatsParticipation = "/repos/:owner/:repo/stats/participation" pathReposStatsPunchCard = "/repos/:owner/:repo/stats/punch_card" pathReposStatuses = "/repos/:owner/:repo/statuses/:ref" pathReposSubscribers = "/repos/:owner/:repo/subscribers" pathReposSubscription = "/repos/:owner/:repo/subscription" pathReposTags = "/repos/:owner/:repo/tags" pathReposTeams = "/repos/:owner/:repo/teams" pathSearchCode = "/search/code" pathSearchIssues = "/search/issues" pathSearchRepositories = "/search/repositories" pathSearchUsers = "/search/users" pathTeamsID = "/teams/:id" pathTeamsMember = "/teams/:id/members/:user" pathTeamsMembers = "/teams/:id/members" pathTeamsRepo = "/teams/:id/repos/:owner/:repo" pathTeamsRepos = "/teams/:id/repos" pathUser = "/user" pathUserEmails = "/user/emails" pathUserFollowers = "/user/followers" pathUserFollowing = "/user/following" pathUserFollowingUser = "/user/following/:user" pathUserIssues = "/user/issues" pathUserKey = "/user/keys/:id" pathUserKeys = "/user/keys" pathUserOrgs = "/user/orgs" pathUserRepos = "/user/repos" pathUserStarred = "/user/starred" pathUserStarredOwnerRepo = "/user/starred/:owner/:repo" pathUserSubscriptions = "/user/subscriptions" pathUserSubscriptionsOwnerRepo = "/user/subscriptions/:owner/:repo" pathUserTeams = "/user/teams" pathUsers = "/users" pathUsersEvents = "/users/:user/events" pathUsersEventsOrgs = "/users/:user/events/orgs/:org" pathUsersEventsPublic = "/users/:user/events/public" pathUsersFollowers = "/users/:user/followers" pathUsersFollowing = "/users/:user/following" pathUsersFollowingTarget = "/users/:user/following/:target_user" pathUsersGists = "/users/:user/gists" pathUsersKeys = "/users/:user/keys" pathUsersOrgs = "/users/:user/orgs" pathUsersReceivedEvents = "/users/:user/received_events" pathUsersReceivedEventsPublic = "/users/:user/received_events/public" pathUsersRepos = "/users/:user/repos" pathUsersStarred = "/users/:user/starred" pathUsersSubscriptions = "/users/:user/subscriptions" pathUsersUser = "/users/:user" ) const ( paramActivityID = "activityId" paramCollection = "collection" paramID = "id" paramKeyword = "keyword" paramName1 = "name1" paramNumber = "number" paramOrg = "org" paramOwner = "owner" paramParam1 = "param1" paramParam2 = "param2" paramRepo = "repo" paramSHA = "sha" paramUser = "user" paramUserID = "userId" paramValue = "value" ) const ( valDenco = "denco" valFoo = "foo" valMe = "me" valNaoina = "naoina" valSHA1 = "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9" valSomething = "something" valTest = "test" valVault = "vault" ) go-openapi-runtime-decad8f/middleware/denco/router.go000066400000000000000000000277021520232310000231410ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2014 Naoya Inada // SPDX-License-Identifier: MIT // Package denco provides fast URL router. package denco import ( "errors" "fmt" "slices" "sort" "strings" ) const ( // ParamCharacter is a special character for path parameter. ParamCharacter = ':' // WildcardCharacter is a special character for wildcard path parameter. WildcardCharacter = '*' // TerminationCharacter is a special character for end of path. TerminationCharacter = '#' // SeparatorCharacter separates path segments. SeparatorCharacter = '/' // PathParamCharacter indicates a RESTCONF path param. PathParamCharacter = '=' // MaxSize is the maximum size of records and internal slice (encoded over 22 bits). MaxSize = (1 << baseBits) - 1 ) // Router represents a URL router. type Router struct { param *doubleArray // SizeHint expects the maximum number of path parameters in records to Build. // SizeHint will be used to determine the capacity of the memory to allocate. // By default, SizeHint will be determined from given records to Build. SizeHint int static map[string]any } // New returns a new Router. func New() *Router { return &Router{ SizeHint: -1, static: make(map[string]any), param: newDoubleArray(), } } // Lookup returns data and path parameters which are associated to the path. // // params is a slice of the [Param] that arranged in the order in which parameters appeared. // // e.g. when built routing path is "/path/to/:id/:name" and given path is "/path/to/1/alice", // params order is [{"id": "1"}, {"name": "alice"}], not [{"name": "alice"}, {"id": "1"}]. func (rt *Router) Lookup(path string) (data any, params Params, found bool) { if data, found = rt.static[path]; found { return data, nil, true } if len(rt.param.node) == 1 { return nil, nil, false } nd, params, found := rt.param.lookup(path, make([]Param, 0, rt.SizeHint), 1) if !found { return nil, nil, false } for i := range params { params[i].Name = nd.paramNames[i] } return nd.data, params, true } // Build builds URL router from records. func (rt *Router) Build(records []Record) error { statics, params := makeRecords(records) if len(params) > MaxSize { return errors.New("denco: too many records") } if rt.SizeHint < 0 { rt.SizeHint = 0 for _, p := range params { size := 0 for _, k := range p.Key { if k == ParamCharacter || k == WildcardCharacter { size++ } } if size > rt.SizeHint { rt.SizeHint = size } } } for _, r := range statics { rt.static[r.Key] = r.Value } if err := rt.param.build(params, 1, 0, make(map[int]struct{})); err != nil { return err } return nil } // Param represents name and value of path parameter. type Param struct { Name string Value string } // Params represents the name and value of path parameters. type Params []Param // Get gets the first value associated with the given name. // If there are no values associated with the key, Get returns "". func (ps Params) Get(name string) string { for _, p := range ps { if p.Name == name { return p.Value } } return "" } type doubleArray struct { bc []baseCheck node []*node } func newDoubleArray() *doubleArray { return &doubleArray{ bc: []baseCheck{0}, node: []*node{nil}, // A start index is adjusting to 1 because 0 will be used as a mark of non-existent node. } } // baseCheck contains BASE, CHECK and Extra flags. // From the top, 22bits of BASE, 2bits of Extra flags and 8bits of CHECK. // // BASE (22bit) | Extra flags (2bit) | CHECK (8bit) // // |----------------------|--|--------| // 32 10 8 0. type baseCheck uint32 const ( baseBits = 22 flagsBits = 10 checkBits = 8 ) func (bc baseCheck) Base() int { return int(bc >> flagsBits) } func (bc *baseCheck) SetBase(base int) { *bc |= baseCheck(base) << flagsBits //nolint:gosec // integer conversion is ok } func (bc baseCheck) Check() byte { return byte(bc) //nolint:gosec // integer conversion is ok: we pick the last 8 bits } func (bc *baseCheck) SetCheck(check byte) { *bc |= baseCheck(check) } func (bc baseCheck) IsEmpty() bool { return bc&0xfffffcff == 0 } func (bc baseCheck) IsSingleParam() bool { return bc¶mTypeSingle == paramTypeSingle } func (bc baseCheck) IsWildcardParam() bool { return bc¶mTypeWildcard == paramTypeWildcard } func (bc baseCheck) IsAnyParam() bool { return bc¶mTypeAny != 0 } func (bc *baseCheck) SetSingleParam() { *bc |= (1 << checkBits) } func (bc *baseCheck) SetWildcardParam() { *bc |= (1 << (checkBits + 1)) } const ( paramTypeSingle = 0x0100 paramTypeWildcard = 0x0200 paramTypeAny = 0x0300 indexOffset = 32 indexMask = uint64(0xffffffff) ) func (da *doubleArray) lookup(path string, params []Param, idx int) (*node, []Param, bool) { indices := make([]uint64, 0, 1) for i := range len(path) { if da.bc[idx].IsAnyParam() { indices = append(indices, (uint64(i)<= len(da.bc) || da.bc[idx].Check() != c { goto BACKTRACKING } } if next := nextIndex(da.bc[idx].Base(), TerminationCharacter); next < len(da.bc) && da.bc[next].Check() == TerminationCharacter { return da.node[da.bc[next].Base()], params, true } BACKTRACKING: for _, j := range slices.Backward(indices) { i, idx := int(j>>indexOffset), int(j&indexMask) if da.bc[idx].IsSingleParam() { nextIdx := nextIndex(da.bc[idx].Base(), ParamCharacter) if nextIdx >= len(da.bc) { break } next := NextSeparator(path, i) nextParams := params nextParams = append(nextParams, Param{Value: path[i:next]}) if nd, nextNextParams, found := da.lookup(path[next:], nextParams, nextIdx); found { return nd, nextNextParams, true } } if da.bc[idx].IsWildcardParam() { nextIdx := nextIndex(da.bc[idx].Base(), WildcardCharacter) nextParams := params nextParams = append(nextParams, Param{Value: path[i:]}) return da.node[da.bc[nextIdx].Base()], nextParams, true } } return nil, nil, false } // build builds double-array from records. func (da *doubleArray) build(srcs []*record, idx, depth int, usedBase map[int]struct{}) error { sort.Stable(recordSlice(srcs)) base, siblings, leaf, err := da.arrange(srcs, idx, depth, usedBase) if err != nil { return err } if leaf != nil { nd, err := makeNode(leaf) if err != nil { return err } da.bc[idx].SetBase(len(da.node)) da.node = append(da.node, nd) } for _, sib := range siblings { da.setCheck(nextIndex(base, sib.c), sib.c) } for _, sib := range siblings { records := srcs[sib.start:sib.end] switch sib.c { case ParamCharacter: for _, r := range records { next := NextSeparator(r.Key, depth+1) name := r.Key[depth+1 : next] r.paramNames = append(r.paramNames, name) r.Key = r.Key[next:] } da.bc[idx].SetSingleParam() if err := da.build(records, nextIndex(base, sib.c), 0, usedBase); err != nil { return err } case WildcardCharacter: r := records[0] name := r.Key[depth+1 : len(r.Key)-1] r.paramNames = append(r.paramNames, name) r.Key = "" da.bc[idx].SetWildcardParam() if err := da.build(records, nextIndex(base, sib.c), 0, usedBase); err != nil { return err } default: if err := da.build(records, nextIndex(base, sib.c), depth+1, usedBase); err != nil { return err } } } return nil } // setBase sets BASE. func (da *doubleArray) setBase(i, base int) { da.bc[i].SetBase(base) } // setCheck sets CHECK. func (da *doubleArray) setCheck(i int, check byte) { da.bc[i].SetCheck(check) } // findEmptyIndex returns an index of unused BASE/CHECK node. func (da *doubleArray) findEmptyIndex(start int) int { i := start for ; i < len(da.bc); i++ { if da.bc[i].IsEmpty() { break } } return i } // findBase returns good BASE. func (da *doubleArray) findBase(siblings []sibling, start int, usedBase map[int]struct{}) (base int) { for idx, firstChar := start+1, siblings[0].c; ; idx = da.findEmptyIndex(idx + 1) { base = nextIndex(idx, firstChar) if _, used := usedBase[base]; used { continue } i := 0 for ; i < len(siblings); i++ { next := nextIndex(base, siblings[i].c) if len(da.bc) <= next { da.bc = append(da.bc, make([]baseCheck, next-len(da.bc)+1)...) } if !da.bc[next].IsEmpty() { break } } if i == len(siblings) { break } } usedBase[base] = struct{}{} return base } func (da *doubleArray) arrange(records []*record, idx, depth int, usedBase map[int]struct{}) (base int, siblings []sibling, leaf *record, err error) { siblings, leaf, err = makeSiblings(records, depth) if err != nil { return -1, nil, nil, err } if len(siblings) < 1 { return -1, nil, leaf, nil } base = da.findBase(siblings, idx, usedBase) if base > MaxSize { return -1, nil, nil, errors.New("denco: too many elements of internal slice") } da.setBase(idx, base) return base, siblings, leaf, err } // node represents a node of Double-Array. type node struct { data any // Names of path parameters. paramNames []string } // makeNode returns a new node from record. func makeNode(r *record) (*node, error) { dups := make(map[string]bool) for _, name := range r.paramNames { if dups[name] { return nil, fmt.Errorf("denco: path parameter `%v' is duplicated in the key `%v'", name, r.Key) } dups[name] = true } return &node{data: r.Value, paramNames: r.paramNames}, nil } // sibling represents an intermediate data of build for Double-Array. type sibling struct { // An index of start of duplicated characters. start int // An index of end of duplicated characters. end int // A character of sibling. c byte } // nextIndex returns a next index of array of BASE/CHECK. func nextIndex(base int, c byte) int { return base ^ int(c) } // makeSiblings returns slice of sibling. func makeSiblings(records []*record, depth int) (sib []sibling, leaf *record, err error) { var ( pc byte n int ) for i, r := range records { if len(r.Key) <= depth { leaf = r continue } c := r.Key[depth] switch { case pc < c: sib = append(sib, sibling{start: i, c: c}) case pc == c: continue default: return nil, nil, errors.New("denco: BUG: routing table hasn't been sorted") } if n > 0 { sib[n-1].end = i } pc = c n++ } if n == 0 { return nil, leaf, nil } sib[n-1].end = len(records) return sib, leaf, nil } // Record represents a record data for router construction. type Record struct { // Key for router construction. Key string // Result value for Key. Value any } // NewRecord returns a new Record. func NewRecord(key string, value any) Record { return Record{ Key: key, Value: value, } } // record represents a record that use to build the Double-Array. type record struct { Record paramNames []string } // makeRecords returns the records that use to build Double-Arrays. func makeRecords(srcs []Record) (statics, params []*record) { termChar := string(TerminationCharacter) paramPrefix := string(SeparatorCharacter) + string(ParamCharacter) wildcardPrefix := string(SeparatorCharacter) + string(WildcardCharacter) restconfPrefix := string(PathParamCharacter) + string(ParamCharacter) for _, r := range srcs { if strings.Contains(r.Key, paramPrefix) || strings.Contains(r.Key, wildcardPrefix) || strings.Contains(r.Key, restconfPrefix) { r.Key += termChar params = append(params, &record{Record: r}) } else { statics = append(statics, &record{Record: r}) } } return statics, params } // recordSlice represents a slice of Record for sort and implements the sort.Interface. type recordSlice []*record // Len implements the sort.Interface.Len. func (rs recordSlice) Len() int { return len(rs) } // Less implements the sort.Interface.Less. func (rs recordSlice) Less(i, j int) bool { return rs[i].Key < rs[j].Key } // Swap implements the sort.Interface.Swap. func (rs recordSlice) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] } go-openapi-runtime-decad8f/middleware/denco/router_bench_test.go000066400000000000000000000111511520232310000253260ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2014 Naoya Inada // SPDX-License-Identifier: MIT package denco_test import ( "bytes" "crypto/rand" "fmt" "math/big" "testing" "github.com/go-openapi/runtime/middleware/denco" ) func BenchmarkRouterLookupStatic100(b *testing.B) { benchmarkRouterLookupStatic(b, 100) } func BenchmarkRouterLookupStatic300(b *testing.B) { benchmarkRouterLookupStatic(b, 300) } func BenchmarkRouterLookupStatic700(b *testing.B) { benchmarkRouterLookupStatic(b, 700) } func BenchmarkRouterLookupSingleParam100(b *testing.B) { records := makeTestSingleParamRecords(100) benchmarkRouterLookupSingleParam(b, records) } func BenchmarkRouterLookupSingleParam300(b *testing.B) { records := makeTestSingleParamRecords(300) benchmarkRouterLookupSingleParam(b, records) } func BenchmarkRouterLookupSingleParam700(b *testing.B) { records := makeTestSingleParamRecords(700) benchmarkRouterLookupSingleParam(b, records) } func BenchmarkRouterLookupSingle2Param100(b *testing.B) { records := makeTestSingle2ParamRecords(100) benchmarkRouterLookupSingleParam(b, records) } func BenchmarkRouterLookupSingle2Param300(b *testing.B) { records := makeTestSingle2ParamRecords(300) benchmarkRouterLookupSingleParam(b, records) } func BenchmarkRouterLookupSingle2Param700(b *testing.B) { records := makeTestSingle2ParamRecords(700) benchmarkRouterLookupSingleParam(b, records) } func BenchmarkRouterBuildStatic100(b *testing.B) { records := makeTestStaticRecords(100) benchmarkRouterBuild(b, records) } func BenchmarkRouterBuildStatic300(b *testing.B) { records := makeTestStaticRecords(300) benchmarkRouterBuild(b, records) } func BenchmarkRouterBuildStatic700(b *testing.B) { records := makeTestStaticRecords(700) benchmarkRouterBuild(b, records) } func BenchmarkRouterBuildSingleParam100(b *testing.B) { records := makeTestSingleParamRecords(100) benchmarkRouterBuild(b, records) } func BenchmarkRouterBuildSingleParam300(b *testing.B) { records := makeTestSingleParamRecords(300) benchmarkRouterBuild(b, records) } func BenchmarkRouterBuildSingleParam700(b *testing.B) { records := makeTestSingleParamRecords(700) benchmarkRouterBuild(b, records) } func BenchmarkRouterBuildSingle2Param100(b *testing.B) { records := makeTestSingle2ParamRecords(100) benchmarkRouterBuild(b, records) } func BenchmarkRouterBuildSingle2Param300(b *testing.B) { records := makeTestSingle2ParamRecords(300) benchmarkRouterBuild(b, records) } func BenchmarkRouterBuildSingle2Param700(b *testing.B) { records := makeTestSingle2ParamRecords(700) benchmarkRouterBuild(b, records) } func benchmarkRouterLookupStatic(b *testing.B, n int) { b.StopTimer() router := denco.New() records := makeTestStaticRecords(n) if err := router.Build(records); err != nil { b.Fatal(err) } record := pickTestRecord(records) b.StartTimer() for range b.N { if r, _, _ := router.Lookup(record.Key); r != record.Value { b.Fail() } } } func benchmarkRouterLookupSingleParam(b *testing.B, records []denco.Record) { router := denco.New() if err := router.Build(records); err != nil { b.Fatal(err) } record := pickTestRecord(records) b.ResetTimer() for range b.N { if _, _, found := router.Lookup(record.Key); !found { b.Fail() } } } func benchmarkRouterBuild(b *testing.B, records []denco.Record) { for range b.N { router := denco.New() if err := router.Build(records); err != nil { b.Fatal(err) } } } func makeTestStaticRecords(n int) []denco.Record { records := make([]denco.Record, n) for i := range n { records[i] = denco.NewRecord("/"+randomString(50), fmt.Sprintf("testroute%d", i)) } return records } func makeTestSingleParamRecords(n int) []denco.Record { records := make([]denco.Record, n) for i := range records { records[i] = denco.NewRecord(fmt.Sprintf("/user%d/:name", i), fmt.Sprintf("testroute%d", i)) } return records } func makeTestSingle2ParamRecords(n int) []denco.Record { records := make([]denco.Record, n) for i := range records { records[i] = denco.NewRecord(fmt.Sprintf("/user%d/:name/comment/:id", i), fmt.Sprintf("testroute%d", i)) } return records } func pickTestRecord(records []denco.Record) denco.Record { return records[len(records)/2] } func randomString(n int) string { const srcStrings = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/" var buf bytes.Buffer for range n { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(srcStrings)-1))) if err != nil { panic(err) } buf.WriteByte(srcStrings[num.Int64()]) } return buf.String() } go-openapi-runtime-decad8f/middleware/denco/router_test.go000066400000000000000000000703351520232310000242000ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2014 Naoya Inada // SPDX-License-Identifier: MIT package denco_test import ( "fmt" "math/rand" "reflect" "testing" "github.com/go-openapi/runtime/middleware/denco" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func routes() []denco.Record { return []denco.Record{ {"/", testRoute0}, {pathPathToRoute, testRoute1}, {"/path/to/other", testRoute2}, {"/path/to/route/a", testRoute3}, {"/path/to/:param", "testroute4"}, {"/gists/:param1/foo/:param2", "testroute12"}, {"/gists/:param1/foo/bar", "testroute11"}, {"/:param1/:param2/foo/:param3", "testroute13"}, {"/path/to/wildcard/*routepath", "testroute5"}, {"/path/to/:param1/:param2", "testroute6"}, {"/path/to/:param1/sep/:param2", "testroute7"}, {"/:year/:month/:day", "testroute8"}, {pathUserID, "testroute9"}, {"/a/to/b/:param/*routepath", "testroute10"}, {"/path/with/key=:value", "testroute14"}, } } var realURIs = []denco.Record{ {pathAuthorizations, pathAuthorizations}, {pathAuthorizationsID, pathAuthorizationsID}, {pathAppsTokens, pathAppsTokens}, {pathEvents, pathEvents}, {pathReposEvents, pathReposEvents}, {pathNetworksEvents, pathNetworksEvents}, {pathOrgsEvents, pathOrgsEvents}, {pathUsersReceivedEvents, pathUsersReceivedEvents}, {pathUsersReceivedEventsPublic, pathUsersReceivedEventsPublic}, {pathUsersEvents, pathUsersEvents}, {pathUsersEventsPublic, pathUsersEventsPublic}, {pathUsersEventsOrgs, pathUsersEventsOrgs}, {pathFeeds, pathFeeds}, {pathNotifications, pathNotifications}, {pathReposNotifications, pathReposNotifications}, {pathNotificationThreads, pathNotificationThreads}, {pathNotificationThreadSub, pathNotificationThreadSub}, {pathReposStargazers, pathReposStargazers}, {pathUsersStarred, pathUsersStarred}, {pathUserStarred, pathUserStarred}, {pathUserStarredOwnerRepo, pathUserStarredOwnerRepo}, {pathReposSubscribers, pathReposSubscribers}, {pathUsersSubscriptions, pathUsersSubscriptions}, {pathUserSubscriptions, pathUserSubscriptions}, {pathReposSubscription, pathReposSubscription}, {pathUserSubscriptionsOwnerRepo, pathUserSubscriptionsOwnerRepo}, {pathUsersGists, pathUsersGists}, {pathGists, pathGists}, {pathGistsID, pathGistsID}, {pathGistsIDStar, pathGistsIDStar}, {pathReposGitBlobs, pathReposGitBlobs}, {pathReposGitCommits, pathReposGitCommits}, {pathReposGitRefs, pathReposGitRefs}, {pathReposGitTags, pathReposGitTags}, {pathReposGitTrees, pathReposGitTrees}, {pathIssues, pathIssues}, {pathUserIssues, pathUserIssues}, {pathOrgsIssues, pathOrgsIssues}, {pathReposIssues, pathReposIssues}, {pathReposIssue, pathReposIssue}, {pathReposAssignees, pathReposAssignees}, {pathReposAssignee, pathReposAssignee}, {pathReposIssueComments, pathReposIssueComments}, {pathReposIssueEvents, pathReposIssueEvents}, {pathReposLabels, pathReposLabels}, {pathReposLabel, pathReposLabel}, {pathReposIssueLabels, pathReposIssueLabels}, {pathReposMilestoneLabels, pathReposMilestoneLabels}, {pathReposMilestones, pathReposMilestones}, {pathReposMilestone, pathReposMilestone}, {pathEmojis, pathEmojis}, {pathGitignoreTemplates, pathGitignoreTemplates}, {pathGitignoreTemplate, pathGitignoreTemplate}, {pathMeta, pathMeta}, {pathRateLimit, pathRateLimit}, {pathUsersOrgs, pathUsersOrgs}, {pathUserOrgs, pathUserOrgs}, {pathOrgsOrg, pathOrgsOrg}, {pathOrgsMembers, pathOrgsMembers}, {pathOrgsMember, pathOrgsMember}, {pathOrgsPublicMembers, pathOrgsPublicMembers}, {pathOrgsPublicMember, pathOrgsPublicMember}, {pathOrgsTeams, pathOrgsTeams}, {pathTeamsID, pathTeamsID}, {pathTeamsMembers, pathTeamsMembers}, {pathTeamsMember, pathTeamsMember}, {pathTeamsRepos, pathTeamsRepos}, {pathTeamsRepo, pathTeamsRepo}, {pathUserTeams, pathUserTeams}, {pathReposPulls, pathReposPulls}, {pathReposPull, pathReposPull}, {pathReposPullCommits, pathReposPullCommits}, {pathReposPullFiles, pathReposPullFiles}, {pathReposPullMerge, pathReposPullMerge}, {pathReposPullComments, pathReposPullComments}, {pathUserRepos, pathUserRepos}, {pathUsersRepos, pathUsersRepos}, {pathOrgsRepos, pathOrgsRepos}, {pathRepositories, pathRepositories}, {pathRepoOwnerRepo, pathRepoOwnerRepo}, {pathReposContributors, pathReposContributors}, {pathReposLanguages, pathReposLanguages}, {pathReposTeams, pathReposTeams}, {pathReposTags, pathReposTags}, {pathReposBranches, pathReposBranches}, {pathReposBranch, pathReposBranch}, {pathReposCollaborators, pathReposCollaborators}, {pathReposCollaborator, pathReposCollaborator}, {pathReposComments, pathReposComments}, {pathReposCommitsSHAComments, pathReposCommitsSHAComments}, {pathReposComment, pathReposComment}, {pathReposCommits, pathReposCommits}, {pathReposCommit, pathReposCommit}, {pathReposReadme, pathReposReadme}, {pathReposKeys, pathReposKeys}, {pathReposKey, pathReposKey}, {pathReposDownloads, pathReposDownloads}, {pathReposDownload, pathReposDownload}, {pathReposForks, pathReposForks}, {pathReposHooks, pathReposHooks}, {pathReposHook, pathReposHook}, {pathReposReleases, pathReposReleases}, {pathReposRelease, pathReposRelease}, {pathReposReleaseAssets, pathReposReleaseAssets}, {pathReposStatsContributors, pathReposStatsContributors}, {pathReposStatsCommitActivity, pathReposStatsCommitActivity}, {pathReposStatsCodeFrequency, pathReposStatsCodeFrequency}, {pathReposStatsParticipation, pathReposStatsParticipation}, {pathReposStatsPunchCard, pathReposStatsPunchCard}, {pathReposStatuses, pathReposStatuses}, {pathSearchRepositories, pathSearchRepositories}, {pathSearchCode, pathSearchCode}, {pathSearchIssues, pathSearchIssues}, {pathSearchUsers, pathSearchUsers}, {pathLegacyIssuesSearch, pathLegacyIssuesSearch}, {pathLegacyReposSearch, pathLegacyReposSearch}, {pathLegacyUserSearch, pathLegacyUserSearch}, {pathLegacyUserEmail, pathLegacyUserEmail}, {pathUsersUser, pathUsersUser}, {pathUser, pathUser}, {pathUsers, pathUsers}, {pathUserEmails, pathUserEmails}, {pathUsersFollowers, pathUsersFollowers}, {pathUserFollowers, pathUserFollowers}, {pathUsersFollowing, pathUsersFollowing}, {pathUserFollowing, pathUserFollowing}, {pathUserFollowingUser, pathUserFollowingUser}, {pathUsersFollowingTarget, pathUsersFollowingTarget}, {pathUsersKeys, pathUsersKeys}, {pathUserKeys, pathUserKeys}, {pathUserKey, pathUserKey}, {pathPeopleUserID, pathPeopleUserID}, {pathPeople, pathPeople}, {pathActivitiesPeople, pathActivitiesPeople}, {pathPeoplePeople, pathPeoplePeople}, {pathPeopleOpenIDConnect, pathPeopleOpenIDConnect}, {pathPeopleActivities, pathPeopleActivities}, {pathActivitiesActivityID, pathActivitiesActivityID}, {pathActivities, pathActivities}, {pathActivitiesComments, pathActivitiesComments}, {pathCommentsCommentID, pathCommentsCommentID}, {pathPeopleMoments, pathPeopleMoments}, } type testcase struct { path string value any params []denco.Param found bool } func runLookupTest(t *testing.T, records []denco.Record, testcases []testcase) { r := denco.New() if err := r.Build(records); err != nil { t.Fatal(err) } for _, testcase := range testcases { data, params, found := r.Lookup(testcase.path) if !reflect.DeepEqual(data, testcase.value) || !reflect.DeepEqual(params, denco.Params(testcase.params)) || !reflect.DeepEqual(found, testcase.found) { t.Errorf("Router.Lookup(%q) => (%#v, %#v, %#v), want (%#v, %#v, %#v)", testcase.path, data, params, found, testcase.value, denco.Params(testcase.params), testcase.found, ) } } } func TestRouter_Lookup(t *testing.T) { testcases := []testcase{ {"/", testRoute0, nil, true}, {"/gists/1323/foo/bar", "testroute11", []denco.Param{{paramParam1, "1323"}}, true}, {"/gists/1323/foo/133", "testroute12", []denco.Param{{paramParam1, "1323"}, {paramParam2, "133"}}, true}, {"/234/1323/foo/133", "testroute13", []denco.Param{{paramParam1, "234"}, {paramParam2, "1323"}, {"param3", "133"}}, true}, {pathPathToRoute, testRoute1, nil, true}, {"/path/to/other", testRoute2, nil, true}, {"/path/to/route/a", testRoute3, nil, true}, {"/path/to/hoge", "testroute4", []denco.Param{{"param", "hoge"}}, true}, {"/path/to/wildcard/some/params", "testroute5", []denco.Param{{"routepath", "some/params"}}, true}, {"/path/to/o1/o2", "testroute6", []denco.Param{{paramParam1, "o1"}, {paramParam2, "o2"}}, true}, {"/path/to/p1/sep/p2", "testroute7", []denco.Param{{paramParam1, "p1"}, {paramParam2, "p2"}}, true}, {"/2014/01/06", "testroute8", []denco.Param{{"year", "2014"}, {"month", "01"}, {"day", "06"}}, true}, {"/user/777", "testroute9", []denco.Param{{paramID, "777"}}, true}, {"/a/to/b/p1/some/wildcard/params", "testroute10", []denco.Param{{"param", "p1"}, {"routepath", "some/wildcard/params"}}, true}, {"/missing", nil, nil, false}, {"/path/with/key=value", "testroute14", []denco.Param{{paramValue, paramValue}}, true}, } runLookupTest(t, routes(), testcases) records := []denco.Record{ {"/", testRoute0}, {"/:b", testRoute1}, {"/*wildcard", testRoute2}, } testcases = []testcase{ {"/", testRoute0, nil, true}, {"/true", testRoute1, []denco.Param{{"b", "true"}}, true}, {"/foo/bar", testRoute2, []denco.Param{{"wildcard", "foo/bar"}}, true}, } runLookupTest(t, records, testcases) records = []denco.Record{ {pathNetworksEvents, testRoute0}, {pathOrgsEvents, testRoute1}, {pathNotificationThreads, testRoute2}, {"/mypathisgreat/:thing-id", testRoute3}, } testcases = []testcase{ {pathNetworksEvents, testRoute0, []denco.Param{{paramOwner, ":owner"}, {paramRepo, ":repo"}}, true}, {pathOrgsEvents, testRoute1, []denco.Param{{paramOrg, ":org"}}, true}, {pathNotificationThreads, testRoute2, []denco.Param{{paramID, ":id"}}, true}, {"/mypathisgreat/:thing-id", testRoute3, []denco.Param{{"thing-id", ":thing-id"}}, true}, } runLookupTest(t, records, testcases) runLookupTest(t, []denco.Record{ {"/", "route2"}, }, []testcase{ {pathUserAlice, nil, nil, false}, }) runLookupTest(t, []denco.Record{ {"/user/:name", "route1"}, }, []testcase{ {"/", nil, nil, false}, }) runLookupTest(t, []denco.Record{ {"/*wildcard", testRoute0}, {"/a/:b", testRoute1}, }, []testcase{ {"/a", testRoute0, []denco.Param{{"wildcard", "a"}}, true}, }) } func TestRouter_Lookup_withManyRoutes(t *testing.T) { n := 1000 records := make([]denco.Record, n) for i := range n { records[i] = denco.Record{Key: "/" + randomString(rand.Intn(50)+10), Value: fmt.Sprintf("route%d", i)} //#nosec } router := denco.New() require.NoError(t, router.Build(records)) for _, r := range records { data, params, found := router.Lookup(r.Key) assert.Equal(t, r.Value, data) assert.Empty(t, params) assert.TrueT(t, found) } } func TestRouter_Lookup_realURIs(t *testing.T) { testcases := []testcase{ {pathAuthorizations, pathAuthorizations, nil, true}, {"/authorizations/1", pathAuthorizationsID, []denco.Param{{paramID, "1"}}, true}, {"/applications/1/tokens/zohRoo7e", pathAppsTokens, []denco.Param{{"client_id", "1"}, {"access_token", "zohRoo7e"}}, true}, {pathEvents, pathEvents, nil, true}, {"/repos/naoina/denco/events", pathReposEvents, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/networks/naoina/denco/events", pathNetworksEvents, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/orgs/something/events", pathOrgsEvents, []denco.Param{{paramOrg, valSomething}}, true}, {"/users/naoina/received_events", pathUsersReceivedEvents, []denco.Param{{paramUser, valNaoina}}, true}, {"/users/naoina/received_events/public", pathUsersReceivedEventsPublic, []denco.Param{{paramUser, valNaoina}}, true}, {"/users/naoina/events", pathUsersEvents, []denco.Param{{paramUser, valNaoina}}, true}, {"/users/naoina/events/public", pathUsersEventsPublic, []denco.Param{{paramUser, valNaoina}}, true}, {"/users/naoina/events/orgs/something", pathUsersEventsOrgs, []denco.Param{{paramUser, valNaoina}, {paramOrg, valSomething}}, true}, {pathFeeds, pathFeeds, nil, true}, {pathNotifications, pathNotifications, nil, true}, {"/repos/naoina/denco/notifications", pathReposNotifications, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/notifications/threads/1", pathNotificationThreads, []denco.Param{{paramID, "1"}}, true}, {"/notifications/threads/2/subscription", pathNotificationThreadSub, []denco.Param{{paramID, "2"}}, true}, {"/repos/naoina/denco/stargazers", pathReposStargazers, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/users/naoina/starred", pathUsersStarred, []denco.Param{{paramUser, valNaoina}}, true}, {pathUserStarred, pathUserStarred, nil, true}, {"/user/starred/naoina/denco", pathUserStarredOwnerRepo, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/subscribers", pathReposSubscribers, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/users/naoina/subscriptions", pathUsersSubscriptions, []denco.Param{{paramUser, valNaoina}}, true}, {pathUserSubscriptions, pathUserSubscriptions, nil, true}, {"/repos/naoina/denco/subscription", pathReposSubscription, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/user/subscriptions/naoina/denco", pathUserSubscriptionsOwnerRepo, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/users/naoina/gists", pathUsersGists, []denco.Param{{paramUser, valNaoina}}, true}, {pathGists, pathGists, nil, true}, {"/gists/1", pathGistsID, []denco.Param{{paramID, "1"}}, true}, {"/gists/2/star", pathGistsIDStar, []denco.Param{{paramID, "2"}}, true}, {"/repos/naoina/denco/git/blobs/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", pathReposGitBlobs, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramSHA, valSHA1}}, true, }, {"/repos/naoina/denco/git/commits/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", pathReposGitCommits, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramSHA, valSHA1}}, true, }, {"/repos/naoina/denco/git/refs", pathReposGitRefs, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/git/tags/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", pathReposGitTags, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramSHA, valSHA1}}, true, }, {"/repos/naoina/denco/git/trees/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", pathReposGitTrees, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramSHA, valSHA1}}, true, }, {pathIssues, pathIssues, nil, true}, {pathUserIssues, pathUserIssues, nil, true}, {"/orgs/something/issues", pathOrgsIssues, []denco.Param{{paramOrg, valSomething}}, true}, {"/repos/naoina/denco/issues", pathReposIssues, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/issues/1", pathReposIssue, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true}, {"/repos/naoina/denco/assignees", pathReposAssignees, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/assignees/foo", pathReposAssignee, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {"assignee", valFoo}}, true, }, {"/repos/naoina/denco/issues/1/comments", pathReposIssueComments, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true}, {"/repos/naoina/denco/issues/1/events", pathReposIssueEvents, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true}, {"/repos/naoina/denco/labels", pathReposLabels, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/labels/bug", pathReposLabel, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {"name", "bug"}}, true}, {"/repos/naoina/denco/issues/1/labels", pathReposIssueLabels, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true}, {"/repos/naoina/denco/milestones/1/labels", pathReposMilestoneLabels, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true, }, {"/repos/naoina/denco/milestones", pathReposMilestones, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/milestones/1", pathReposMilestone, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true, }, {pathEmojis, pathEmojis, nil, true}, {pathGitignoreTemplates, pathGitignoreTemplates, nil, true}, {"/gitignore/templates/Go", pathGitignoreTemplate, []denco.Param{{"name", "Go"}}, true}, {pathMeta, pathMeta, nil, true}, {pathRateLimit, pathRateLimit, nil, true}, {"/users/naoina/orgs", pathUsersOrgs, []denco.Param{{paramUser, valNaoina}}, true}, {pathUserOrgs, pathUserOrgs, nil, true}, {"/orgs/something", pathOrgsOrg, []denco.Param{{paramOrg, valSomething}}, true}, {"/orgs/something/members", pathOrgsMembers, []denco.Param{{paramOrg, valSomething}}, true}, {"/orgs/something/members/naoina", pathOrgsMember, []denco.Param{{paramOrg, valSomething}, {paramUser, valNaoina}}, true}, {"/orgs/something/public_members", pathOrgsPublicMembers, []denco.Param{{paramOrg, valSomething}}, true}, {"/orgs/something/public_members/naoina", pathOrgsPublicMember, []denco.Param{{paramOrg, valSomething}, {paramUser, valNaoina}}, true}, {"/orgs/something/teams", pathOrgsTeams, []denco.Param{{paramOrg, valSomething}}, true}, {"/teams/1", pathTeamsID, []denco.Param{{paramID, "1"}}, true}, {"/teams/2/members", pathTeamsMembers, []denco.Param{{paramID, "2"}}, true}, {"/teams/3/members/naoina", pathTeamsMember, []denco.Param{{paramID, "3"}, {paramUser, valNaoina}}, true}, {"/teams/4/repos", pathTeamsRepos, []denco.Param{{paramID, "4"}}, true}, {"/teams/5/repos/naoina/denco", pathTeamsRepo, []denco.Param{{paramID, "5"}, {paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {pathUserTeams, pathUserTeams, nil, true}, {"/repos/naoina/denco/pulls", pathReposPulls, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/pulls/1", pathReposPull, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true}, {"/repos/naoina/denco/pulls/1/commits", pathReposPullCommits, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true}, {"/repos/naoina/denco/pulls/1/files", pathReposPullFiles, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true}, {"/repos/naoina/denco/pulls/1/merge", pathReposPullMerge, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true}, {"/repos/naoina/denco/pulls/1/comments", pathReposPullComments, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramNumber, "1"}}, true}, {pathUserRepos, pathUserRepos, nil, true}, {"/users/naoina/repos", pathUsersRepos, []denco.Param{{paramUser, valNaoina}}, true}, {"/orgs/something/repos", pathOrgsRepos, []denco.Param{{paramOrg, valSomething}}, true}, {pathRepositories, pathRepositories, nil, true}, {"/repos/naoina/denco", pathRepoOwnerRepo, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/contributors", pathReposContributors, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/languages", pathReposLanguages, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/teams", pathReposTeams, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/tags", pathReposTags, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/branches", pathReposBranches, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/branches/master", pathReposBranch, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {"branch", "master"}}, true}, {"/repos/naoina/denco/collaborators", pathReposCollaborators, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/collaborators/something", pathReposCollaborator, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramUser, valSomething}}, true}, {"/repos/naoina/denco/comments", pathReposComments, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/commits/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9/comments", pathReposCommitsSHAComments, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramSHA, valSHA1}}, true, }, {"/repos/naoina/denco/comments/1", pathReposComment, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramID, "1"}}, true}, {"/repos/naoina/denco/commits", pathReposCommits, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/commits/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", pathReposCommit, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramSHA, valSHA1}}, true, }, {"/repos/naoina/denco/readme", pathReposReadme, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/keys", pathReposKeys, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/keys/1", pathReposKey, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramID, "1"}}, true}, {"/repos/naoina/denco/downloads", pathReposDownloads, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/downloads/2", pathReposDownload, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramID, "2"}}, true}, {"/repos/naoina/denco/forks", pathReposForks, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/hooks", pathReposHooks, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/hooks/2", pathReposHook, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramID, "2"}}, true}, {"/repos/naoina/denco/releases", pathReposReleases, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/releases/1", pathReposRelease, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramID, "1"}}, true}, {"/repos/naoina/denco/releases/1/assets", pathReposReleaseAssets, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {paramID, "1"}}, true, }, {"/repos/naoina/denco/stats/contributors", pathReposStatsContributors, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/stats/commit_activity", pathReposStatsCommitActivity, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/stats/code_frequency", pathReposStatsCodeFrequency, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/stats/participation", pathReposStatsParticipation, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/stats/punch_card", pathReposStatsPunchCard, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}}, true}, {"/repos/naoina/denco/statuses/master", pathReposStatuses, []denco.Param{{paramOwner, valNaoina}, {paramRepo, valDenco}, {"ref", "master"}}, true}, {pathSearchRepositories, pathSearchRepositories, nil, true}, {pathSearchCode, pathSearchCode, nil, true}, {pathSearchIssues, pathSearchIssues, nil, true}, {pathSearchUsers, pathSearchUsers, nil, true}, {"/legacy/issues/search/naoina/denco/closed/test", pathLegacyIssuesSearch, []denco.Param{{paramOwner, valNaoina}, {"repository", valDenco}, {"state", "closed"}, {paramKeyword, valTest}}, true, }, {"/legacy/repos/search/test", pathLegacyReposSearch, []denco.Param{{paramKeyword, valTest}}, true}, {"/legacy/user/search/test", pathLegacyUserSearch, []denco.Param{{paramKeyword, valTest}}, true}, {"/legacy/user/email/naoina@kuune.org", pathLegacyUserEmail, []denco.Param{{"email", "naoina@kuune.org"}}, true}, {"/users/naoina", pathUsersUser, []denco.Param{{paramUser, valNaoina}}, true}, {pathUser, pathUser, nil, true}, {pathUsers, pathUsers, nil, true}, {pathUserEmails, pathUserEmails, nil, true}, {"/users/naoina/followers", pathUsersFollowers, []denco.Param{{paramUser, valNaoina}}, true}, {pathUserFollowers, pathUserFollowers, nil, true}, {"/users/naoina/following", pathUsersFollowing, []denco.Param{{paramUser, valNaoina}}, true}, {pathUserFollowing, pathUserFollowing, nil, true}, {"/user/following/naoina", pathUserFollowingUser, []denco.Param{{paramUser, valNaoina}}, true}, {"/users/naoina/following/target", pathUsersFollowingTarget, []denco.Param{{paramUser, valNaoina}, {"target_user", "target"}}, true}, {"/users/naoina/keys", pathUsersKeys, []denco.Param{{paramUser, valNaoina}}, true}, {pathUserKeys, pathUserKeys, nil, true}, {"/user/keys/1", pathUserKey, []denco.Param{{paramID, "1"}}, true}, {"/people/me", pathPeopleUserID, []denco.Param{{paramUserID, valMe}}, true}, {pathPeople, pathPeople, nil, true}, {"/activities/foo/people/vault", pathActivitiesPeople, []denco.Param{{paramActivityID, valFoo}, {paramCollection, valVault}}, true}, {"/people/me/people/vault", pathPeoplePeople, []denco.Param{{paramUserID, valMe}, {paramCollection, valVault}}, true}, {"/people/me/openIdConnect", pathPeopleOpenIDConnect, []denco.Param{{paramUserID, valMe}}, true}, {"/people/me/activities/vault", pathPeopleActivities, []denco.Param{{paramUserID, valMe}, {paramCollection, valVault}}, true}, {"/activities/foo", pathActivitiesActivityID, []denco.Param{{paramActivityID, valFoo}}, true}, {pathActivities, pathActivities, nil, true}, {"/activities/foo/comments", pathActivitiesComments, []denco.Param{{paramActivityID, valFoo}}, true}, {"/comments/hoge", pathCommentsCommentID, []denco.Param{{"commentId", "hoge"}}, true}, {"/people/me/moments/vault", pathPeopleMoments, []denco.Param{{paramUserID, valMe}, {paramCollection, valVault}}, true}, } runLookupTest(t, realURIs, testcases) } func TestRouter_Build(t *testing.T) { // test for duplicate name of path parameters. func() { r := denco.New() require.Errorf(t, r.Build([]denco.Record{ {"/:user/:id/:id", testRoute0}, {"/:user/:user/:id", testRoute0}, }), "no error returned by duplicate name of path parameters", ) }() } func TestRouter_Build_withoutSizeHint(t *testing.T) { for _, v := range []struct { keys []string sizeHint int }{ {[]string{pathUser}, 0}, {[]string{pathUserID}, 1}, {[]string{"/user/:id/post"}, 1}, {[]string{"/user/:id/post:validate"}, 2}, {[]string{pathUserIDGroup}, 2}, {[]string{pathUserIDPostCID}, 2}, {[]string{pathUserIDPostCID, "/admin/:id/post/:cid"}, 2}, {[]string{pathUserID, "/admin/:id/post/:cid"}, 2}, {[]string{pathUserIDPostCID, "/admin/:id/post/:cid/:type"}, 3}, } { r := denco.New() actual := r.SizeHint expected := -1 assert.EqualTf(t, expected, actual, `before Build; Router.SizeHint => (%[1]T=%#[1]v); want (%[2]T=%#[2]v)`, actual, expected) records := make([]denco.Record, len(v.keys)) for i, k := range v.keys { records[i] = denco.Record{Key: k, Value: paramValue} } require.NoError(t, r.Build(records)) actual = r.SizeHint expected = v.sizeHint assert.EqualTf(t, expected, actual, `Router.Build(%#v); Router.SizeHint => (%[2]T=%#[2]v); want (%[3]T=%#[3]v)`, records, actual, expected) } } func TestRouter_Build_withSizeHint(t *testing.T) { for _, v := range []struct { key string sizeHint int expect int }{ {pathUser, 0, 0}, {pathUser, 1, 1}, {pathUser, 2, 2}, {pathUserID, 3, 3}, {pathUserIDGroup, 0, 0}, {pathUserIDGroup, 1, 1}, {"/user/:id/:group:validate", 1, 1}, } { r := denco.New() r.SizeHint = v.sizeHint records := []denco.Record{ {v.key, paramValue}, } require.NoError(t, r.Build(records)) actual := r.SizeHint expected := v.expect assert.EqualTf(t, expected, actual, `Router.Build(%#v); Router.SizeHint => (%[2]T=%#[2]v); want (%[3]T=%#[3]v)`, records, actual, expected) } } func TestParams_Get(t *testing.T) { params := denco.Params([]denco.Param{ {paramName1, "value1"}, {"name2", "value2"}, {"name3", "value3"}, {paramName1, "value4"}, }) for _, v := range []struct{ value, expected string }{ {paramName1, "value1"}, {"name2", "value2"}, {"name3", "value3"}, {"name4", ""}, } { actual := params.Get(v.value) expected := v.expected assert.EqualT(t, expected, actual, "Params.Get(%q) => %#v, want %#v", v.value, actual, expected) } } go-openapi-runtime-decad8f/middleware/denco/server.go000066400000000000000000000061101520232310000231150ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2014 Naoya Inada // SPDX-License-Identifier: MIT package denco import ( "net/http" ) // Mux represents a multiplexer for HTTP requests. type Mux struct{} // NewMux returns a new [Mux]. func NewMux() *Mux { return &Mux{} } // GET is shorthand for [Mux.Handler] ("GET", path, handler). func (m *Mux) GET(path string, handler HandlerFunc) Handler { return m.Handler("GET", path, handler) } // POST is shorthand for [Mux.Handler] ("POST", path, handler). func (m *Mux) POST(path string, handler HandlerFunc) Handler { return m.Handler("POST", path, handler) } // PUT is shorthand for [Mux.Handler] ("PUT", path, handler). func (m *Mux) PUT(path string, handler HandlerFunc) Handler { return m.Handler("PUT", path, handler) } // HEAD is shorthand for [Mux.Handler]("HEAD", path, handler). func (m *Mux) HEAD(path string, handler HandlerFunc) Handler { return m.Handler("HEAD", path, handler) } // Handler returns a [Handler] for a HTTP method. func (m *Mux) Handler(method, path string, handler HandlerFunc) Handler { return Handler{ Method: method, Path: path, Func: handler, } } // Build builds a [http.Handler]. func (m *Mux) Build(handlers []Handler) (http.Handler, error) { recordMap := make(map[string][]Record) for _, h := range handlers { recordMap[h.Method] = append(recordMap[h.Method], NewRecord(h.Path, h.Func)) } mux := newServeMux() for m, records := range recordMap { router := New() if err := router.Build(records); err != nil { return nil, err } mux.routers[m] = router } return mux, nil } // Handler represents a handler of HTTP requests. type Handler struct { // Method is an HTTP method. Method string // Path is a routing path for handler. Path string // Func is a function of handler of HTTP request. Func HandlerFunc } // HandlerFunc is an alias to the handler function, similar to [http.HandlerFunc]. type HandlerFunc func(w http.ResponseWriter, r *http.Request, params Params) type serveMux struct { routers map[string]*Router } func newServeMux() *serveMux { return &serveMux{ routers: make(map[string]*Router), } } // ServeHTTP implements the [http.Handler] interface. func (mux *serveMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { handler, params := mux.handler(r.Method, r.URL.Path) handler(w, r, params) } func (mux *serveMux) handler(method, path string) (HandlerFunc, []Param) { if router, found := mux.routers[method]; found { if handler, params, found := router.Lookup(path); found { return handler.(HandlerFunc), params //nolint:forcetypeassert // type is guaranteed when the path is found } } return NotFound, nil } // NotFound replies to the request with an HTTP 404 not found error. // // NotFound is called when unknown HTTP methods are being user or a handler not found. // // If you want to use your own NotFound handler, please overwrite this variable. var NotFound = func(w http.ResponseWriter, r *http.Request, _ Params) { http.NotFound(w, r) } go-openapi-runtime-decad8f/middleware/denco/server_test.go000066400000000000000000000073531520232310000241660ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2014 Naoya Inada // SPDX-License-Identifier: MIT package denco_test import ( "context" "fmt" "io" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/runtime/middleware/denco" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func testHandlerFunc(w http.ResponseWriter, r *http.Request, params denco.Params) { fmt.Fprintf(w, "method: %s, path: %s, params: %v", r.Method, r.URL.Path, params) //nolint:gosec // test handler, no XSS risk } func TestMux(t *testing.T) { mux := denco.NewMux() handler, err := mux.Build([]denco.Handler{ mux.GET("/", testHandlerFunc), mux.GET("/user/:name", testHandlerFunc), mux.POST("/user/:name", testHandlerFunc), mux.HEAD("/user/:name", testHandlerFunc), mux.PUT("/user/:name", testHandlerFunc), mux.Handler(http.MethodGet, "/user/handler", testHandlerFunc), mux.Handler(http.MethodPost, "/user/handler", testHandlerFunc), mux.Handler(http.MethodPut, "/user/inference", testHandlerFunc), }) require.NoError(t, err) server := httptest.NewServer(handler) defer server.Close() for _, v := range []struct { status int method, path, expected string }{ {http.StatusOK, http.MethodGet, "/", "method: GET, path: /, params: []"}, {http.StatusOK, http.MethodGet, pathUserAlice, "method: GET, path: /user/alice, params: [{name alice}]"}, {http.StatusOK, http.MethodPost, "/user/bob", "method: POST, path: /user/bob, params: [{name bob}]"}, {http.StatusOK, http.MethodHead, pathUserAlice, ""}, {http.StatusOK, http.MethodPut, "/user/bob", "method: PUT, path: /user/bob, params: [{name bob}]"}, {http.StatusNotFound, http.MethodPost, "/", notFoundBody}, {http.StatusNotFound, http.MethodGet, "/unknown", notFoundBody}, {http.StatusNotFound, http.MethodPost, pathUserAlice + "/1", notFoundBody}, {http.StatusOK, http.MethodGet, "/user/handler", "method: GET, path: /user/handler, params: []"}, {http.StatusOK, http.MethodPost, "/user/handler", "method: POST, path: /user/handler, params: []"}, {http.StatusOK, http.MethodPut, "/user/inference", "method: PUT, path: /user/inference, params: []"}, } { req, err := http.NewRequestWithContext(context.Background(), v.method, server.URL+v.path, nil) require.NoError(t, err) res, err := http.DefaultClient.Do(req) require.NoError(t, err) defer res.Body.Close() body, err := io.ReadAll(res.Body) require.NoError(t, err) actual := string(body) expected := v.expected assert.EqualTf(t, v.status, res.StatusCode, "for method %s in path %s", v.method, v.path) assert.EqualTf(t, expected, actual, "for method %s in path %s", v.method, v.path) } } func TestNotFound(t *testing.T) { mux := denco.NewMux() handler, err := mux.Build([]denco.Handler{}) require.NoError(t, err) server := httptest.NewServer(handler) defer server.Close() origNotFound := denco.NotFound defer func() { denco.NotFound = origNotFound }() denco.NotFound = func(w http.ResponseWriter, r *http.Request, params denco.Params) { w.WriteHeader(http.StatusServiceUnavailable) fmt.Fprintf(w, "method: %s, path: %s, params: %v", r.Method, r.URL.Path, params) //nolint:gosec // test handler, no XSS risk } req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil) require.NoError(t, err) res, err := http.DefaultClient.Do(req) require.NoError(t, err) defer res.Body.Close() body, err := io.ReadAll(res.Body) require.NoError(t, err) actual := string(body) expected := "method: GET, path: /, params: []" assert.EqualT(t, http.StatusServiceUnavailable, res.StatusCode) assert.EqualT(t, expected, actual) } go-openapi-runtime-decad8f/middleware/denco/util.go000066400000000000000000000007321520232310000225700ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2014 Naoya Inada // SPDX-License-Identifier: MIT package denco // NextSeparator returns an index of next separator in path. func NextSeparator(path string, start int) int { for start < len(path) { if c := path[start]; c == '/' || c == TerminationCharacter { break } start++ } return start } go-openapi-runtime-decad8f/middleware/denco/util_test.go000066400000000000000000000016471520232310000236350ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2014 Naoya Inada // SPDX-License-Identifier: MIT package denco_test import ( "reflect" "testing" "github.com/go-openapi/runtime/middleware/denco" ) func TestNextSeparator(t *testing.T) { for _, testcase := range []struct { path string start int expected any }{ {pathPathToRoute, 0, 0}, {pathPathToRoute, 1, 5}, {pathPathToRoute, 9, 14}, {"/path.html", 1, 10}, {"/foo/bar.html", 1, 4}, {"/foo/bar.html/baz.png", 5, 13}, {"/foo/bar.html/baz.png", 14, 21}, {"path#", 0, 4}, } { actual := denco.NextSeparator(testcase.path, testcase.start) expected := testcase.expected if !reflect.DeepEqual(actual, expected) { t.Errorf("path = %q, start = %v expect %v, but %v", testcase.path, testcase.start, expected, actual) } } } go-openapi-runtime-decad8f/middleware/doc.go000066400000000000000000000027621520232310000212750ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // Package middleware provides the library with helper functions for serving swagger APIs. // // Pseudo middleware handler. // // import ( // "net/http" // // "github.com/go-openapi/errors" // ) // // func newCompleteMiddleware(ctx *Context) http.Handler { // return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // // use context to lookup routes // if matched, ok := ctx.RouteInfo(r); ok { // // if matched.NeedsAuth() { // if _, err := ctx.Authorize(r, matched); err != nil { // ctx.Respond(rw, r, matched.Produces, matched, err) // return // } // } // // bound, validation := ctx.BindAndValidate(r, matched) // if validation != nil { // ctx.Respond(rw, r, matched.Produces, matched, validation) // return // } // // result, err := matched.Handler.Handle(bound) // if err != nil { // ctx.Respond(rw, r, matched.Produces, matched, err) // return // } // // ctx.Respond(rw, r, matched.Produces, matched, result) // return // } // // // Not found, check if it exists in the other methods first // if others := ctx.AllowedMethods(r); len(others) > 0 { // ctx.Respond(rw, r, ctx.spec.RequiredProduces(), nil, errors.MethodNotAllowed(r.Method, others)) // return // } // ctx.Respond(rw, r, ctx.spec.RequiredProduces(), nil, errors.NotFound("path %s was not found", r.URL.Path)) // }) // } package middleware go-openapi-runtime-decad8f/middleware/header/000077500000000000000000000000001520232310000214225ustar00rootroot00000000000000go-openapi-runtime-decad8f/middleware/header/header.go000066400000000000000000000045021520232310000232020ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // Package header forwards to the relocated implementation at // [github.com/go-openapi/runtime/server-middleware/negotiate/header]. // // Deprecated: this package was unintentionally exposed and has moved to // [github.com/go-openapi/runtime/server-middleware/negotiate/header]. // // The shim preserves the public surface so existing imports keep // compiling against v0.30.x; new code should target the new path. package header import ( "net/http" "time" upstream "github.com/go-openapi/runtime/server-middleware/negotiate/header" ) // AcceptSpec describes an entry parsed from an Accept-style header. // // Deprecated: see package documentation. type AcceptSpec = upstream.AcceptSpec // Copy returns a shallow copy of the header. // // Deprecated: see package documentation. func Copy(header http.Header) http.Header { return upstream.Copy(header) } // ParseList parses a comma separated list of values. // // Commas are ignored in quoted strings. Quoted values are not unescaped or // unquoted. Whitespace is trimmed. // // Deprecated: see package documentation. func ParseList(header http.Header, key string) []string { return upstream.ParseList(header, key) } // ParseTime parses the header as time. // // The zero value is returned if the header is not present or there is an // error parsing the header. // // Deprecated: see package documentation. func ParseTime(header http.Header, key string) time.Time { return upstream.ParseTime(header, key) } // ParseValueAndParams parses a comma separated list of values with optional // semicolon separated name-value pairs. // // Content-Type and Content-Disposition headers are in this format. // // Deprecated: see package documentation. func ParseValueAndParams(header http.Header, key string) (string, map[string]string) { return upstream.ParseValueAndParams(header, key) } // ParseAccept parses Accept* headers. // // Deprecated: see package documentation. func ParseAccept(header http.Header, key string) []AcceptSpec { return upstream.ParseAccept(header, key) } // ParseAccept2 parses Accept* headers (alternate parser). // // Deprecated: see package documentation. func ParseAccept2(header http.Header, key string) (specs []AcceptSpec) { return upstream.ParseAccept2(header, key) } go-openapi-runtime-decad8f/middleware/header/header_test.go000066400000000000000000000054351520232310000242470ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package header_test import ( "net/http" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" // shim under test header "github.com/go-openapi/runtime/middleware/header" upstream "github.com/go-openapi/runtime/server-middleware/negotiate/header" ) // TestShimWiring is a smoke test: it asserts that every exported symbol // re-exported from middleware/header forwards to the relocated package. // Edge-case behaviour is exhaustively covered in the upstream package // (server-middleware/negotiate/header) — the assertions here only need // to be specific enough to prove the call landed there. func TestShimWiring(t *testing.T) { t.Run("AcceptSpec is a type alias for upstream.AcceptSpec", func(t *testing.T) { // Type alias means a value of one is assignable to the other // without conversion. If the shim re-declared the struct we'd // need an explicit cast and this would not compile. The explicit // type annotations are the assertion — do not let inference // erase them. //nolint:staticcheck // ST1023: explicit annotations prove the alias var s header.AcceptSpec = upstream.AcceptSpec{Value: "x", Q: 1.0} //nolint:staticcheck // ST1023: explicit annotations prove the alias var u upstream.AcceptSpec = s assert.EqualT(t, "x", u.Value) assert.InDeltaT(t, 1.0, u.Q, 0) }) t.Run("Copy forwards", func(t *testing.T) { in := http.Header{"X-Test": []string{"v"}} got := header.Copy(in) require.Len(t, got, 1) assert.EqualT(t, "v", got.Get("X-Test")) }) t.Run("ParseList forwards", func(t *testing.T) { got := header.ParseList(http.Header{"X-Test": []string{"a, b"}}, "X-Test") assert.Equal(t, []string{"a", "b"}, got) }) t.Run("ParseTime forwards", func(t *testing.T) { h := http.Header{} h.Set("Date", "Sun, 06 Nov 1994 08:49:37 GMT") got := header.ParseTime(h, "Date") assert.EqualT(t, 1994, got.Year()) }) t.Run("ParseValueAndParams forwards", func(t *testing.T) { h := http.Header{} h.Set("Content-Type", "text/plain; charset=utf-8") value, params := header.ParseValueAndParams(h, "Content-Type") assert.EqualT(t, "text/plain", value) assert.EqualT(t, "utf-8", params["charset"]) }) t.Run("ParseAccept forwards", func(t *testing.T) { got := header.ParseAccept(http.Header{"Accept": []string{"text/html;q=0.5"}}, "Accept") require.Len(t, got, 1) assert.EqualT(t, "text/html", got[0].Value) assert.InDeltaT(t, 0.5, got[0].Q, 1e-9) }) t.Run("ParseAccept2 forwards", func(t *testing.T) { got := header.ParseAccept2(http.Header{"Accept": []string{"text/html;q=0.5"}}, "Accept") require.Len(t, got, 1) assert.EqualT(t, "text/html", got[0].Value) assert.InDeltaT(t, 0.5, got[0].Q, 1e-9) }) } go-openapi-runtime-decad8f/middleware/not_implemented.go000066400000000000000000000023701520232310000237060ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "net/http" "github.com/go-openapi/runtime" ) type errorResp struct { code int response any headers http.Header } func (e *errorResp) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { for k, v := range e.headers { for _, val := range v { rw.Header().Add(k, val) } } if e.code > 0 { rw.WriteHeader(e.code) } else { rw.WriteHeader(http.StatusInternalServerError) } if err := producer.Produce(rw, e.response); err != nil { Logger.Printf("failed to write error response: %v", err) } } // NotImplemented the error response when the response is not implemented. func NotImplemented(message string) Responder { return Error(http.StatusNotImplemented, message) } // Error creates a generic responder for returning errors, the data will be serialized // with the matching producer for the request. func Error(code int, data any, headers ...http.Header) Responder { var hdr http.Header for _, h := range headers { for k, v := range h { if hdr == nil { hdr = make(http.Header) } hdr[k] = v } } return &errorResp{ code: code, response: data, headers: hdr, } } go-openapi-runtime-decad8f/middleware/not_implemented_test.go000066400000000000000000000011531520232310000247430ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "net/http" "net/http/httptest" "testing" "github.com/go-openapi/runtime" "github.com/go-openapi/testify/v2/require" ) func TestErrorResponder(t *testing.T) { resp := Error(http.StatusBadRequest, map[string]string{"message": "this is the error body"}) rec := httptest.NewRecorder() resp.WriteResponse(rec, runtime.JSONProducer()) require.EqualT(t, http.StatusBadRequest, rec.Code) require.JSONEqT(t, "{\"message\":\"this is the error body\"}\n", rec.Body.String()) } go-openapi-runtime-decad8f/middleware/operation.go000066400000000000000000000010171520232310000225200ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import "net/http" // NewOperationExecutor creates a context aware [middleware] that handles the operations after routing. func NewOperationExecutor(ctx *Context) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // use context to lookup routes route, rCtx, _ := ctx.RouteInfo(r) if rCtx != nil { r = rCtx } route.Handler.ServeHTTP(rw, r) }) } go-openapi-runtime-decad8f/middleware/operation_test.go000066400000000000000000000036501520232310000235640ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( stdcontext "context" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/errors" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/internal/testing/petstore" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestOperationExecutor(t *testing.T) { spec, api := petstore.NewAPI(t) api.RegisterOperation("get", "/pets", runtime.OperationHandlerFunc(func(_ any) (any, error) { return []any{ map[string]any{paramKeyID: 1, paramKeyName: "a dog"}, }, nil })) context := NewContext(spec, api, nil) context.router = DefaultRouter(spec, context.api) mw := NewOperationExecutor(context) recorder := httptest.NewRecorder() request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) request.Header.Add("Accept", jsonMime) request.SetBasicAuth("admin", "admin") mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) assert.JSONEqT(t, `[{"id":1,"name":"a dog"}]`+"\n", recorder.Body.String()) spec, api = petstore.NewAPI(t) api.RegisterOperation("get", "/pets", runtime.OperationHandlerFunc(func(_ any) (any, error) { return nil, errors.New(http.StatusUnprocessableEntity, "expected") })) context = NewContext(spec, api, nil) context.router = DefaultRouter(spec, context.api) mw = NewOperationExecutor(context) recorder = httptest.NewRecorder() request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) request.Header.Add("Accept", jsonMime) request.SetBasicAuth("admin", "admin") mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusUnprocessableEntity, recorder.Code) assert.JSONEqT(t, `{"code":422,"message":"expected"}`, recorder.Body.String()) } go-openapi-runtime-decad8f/middleware/parameter.go000066400000000000000000000332661520232310000225130ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "encoding" "encoding/base64" stderrors "errors" "io" "net/http" "reflect" "strconv" "github.com/go-openapi/errors" "github.com/go-openapi/runtime" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag/conv" "github.com/go-openapi/swag/stringutils" "github.com/go-openapi/validate" ) const defaultMaxMemory = 32 << 20 const ( typeString = "string" typeArray = "array" ) var textUnmarshalType = reflect.TypeFor[encoding.TextUnmarshaler]() func newUntypedParamBinder(param spec.Parameter, spec *spec.Swagger, formats strfmt.Registry) *untypedParamBinder { binder := new(untypedParamBinder) binder.Name = param.Name binder.parameter = ¶m binder.formats = formats if param.In != "body" { binder.validator = validate.NewParamValidator(¶m, formats) } else { binder.validator = validate.NewSchemaValidator(param.Schema, spec, param.Name, formats) } return binder } type untypedParamBinder struct { parameter *spec.Parameter formats strfmt.Registry Name string validator validate.EntityValidator } func (p *untypedParamBinder) Type() reflect.Type { return p.typeForSchema(p.parameter.Type, p.parameter.Format, p.parameter.Items) } func (p *untypedParamBinder) Bind(request *http.Request, routeParams RouteParams, consumer runtime.Consumer, target reflect.Value) error { switch p.parameter.In { case "query": return p.bindQuery(request, routeParams, consumer, target) case "header": return p.bindHeader(request, routeParams, consumer, target) case "path": return p.bindPath(request, routeParams, consumer, target) case "formData": return p.bindFormData(request, routeParams, consumer, target) case "body": return p.bindBody(request, routeParams, consumer, target) default: return errors.New(http.StatusInternalServerError, "invalid parameter location: %q", p.parameter.In) } } func (p *untypedParamBinder) bindQuery(request *http.Request, _ RouteParams, _ runtime.Consumer, target reflect.Value) error { data, custom, hasKey, err := p.readValue(runtime.Values(request.URL.Query()), target) if err != nil { return err } if custom { return nil } return p.bindValue(data, hasKey, target) } func (p *untypedParamBinder) bindHeader(request *http.Request, _ RouteParams, _ runtime.Consumer, target reflect.Value) error { data, custom, hasKey, err := p.readValue(runtime.Values(request.Header), target) if err != nil { return err } if custom { return nil } return p.bindValue(data, hasKey, target) } func (p *untypedParamBinder) bindPath(_ *http.Request, routeParams RouteParams, _ runtime.Consumer, target reflect.Value) error { data, custom, hasKey, err := p.readValue(routeParams, target) if err != nil { return err } if custom { return nil } return p.bindValue(data, hasKey, target) } func (p *untypedParamBinder) bindFormData(request *http.Request, _ RouteParams, _ runtime.Consumer, target reflect.Value) error { mt, _, ctErr := runtime.ContentType(request.Header) if ctErr != nil { return errors.InvalidContentType("", []string{runtime.MultipartFormMime, runtime.URLencodedFormMime}) } if mt != runtime.MultipartFormMime && mt != runtime.URLencodedFormMime { return errors.InvalidContentType(mt, []string{runtime.MultipartFormMime, runtime.URLencodedFormMime}) } // Parse via the shared helper. The helper routes on Content-Type // (multipart/form-data → ParseMultipartForm; all non-multipart types, // including application/x-www-form-urlencoded, → ParseForm) // and applies the default 32 MiB body cap via http.MaxBytesReader. // Idempotent across the per-parameter loop: stdlib short-circuits // when r.MultipartForm / r.PostForm are already populated. if _, perr := runtime.BindForm(request, runtime.BindFormMaxParseMemory(defaultMaxMemory)); perr != nil { return perr } if p.parameter.Type == "file" { file, header, ffErr := request.FormFile(p.parameter.Name) if ffErr != nil { if p.parameter.Required { return errors.NewParseError(p.Name, p.parameter.In, "", ffErr) } return nil } // Mirror the FileHeader.Filename length cap that BindForm // applies to typed (codegen) paths through BindFormFile, so // untyped formData bindings get the same protection. if err := runtime.ValidateFilenameLength(p.Name, p.parameter.In, header.Filename, runtime.DefaultMaxUploadFilenameLength); err != nil { return err } target.Set(reflect.ValueOf(runtime.File{Data: file, Header: header})) return nil } if request.MultipartForm != nil { data, custom, hasKey, rvErr := p.readValue(runtime.Values(request.MultipartForm.Value), target) if rvErr != nil { return rvErr } if custom { return nil } return p.bindValue(data, hasKey, target) } data, custom, hasKey, err := p.readValue(runtime.Values(request.PostForm), target) if err != nil { return err } if custom { return nil } return p.bindValue(data, hasKey, target) } func (p *untypedParamBinder) bindBody(request *http.Request, _ RouteParams, consumer runtime.Consumer, target reflect.Value) error { newValue := reflect.New(target.Type()) if !runtime.HasBody(request) { if p.parameter.Default != nil { target.Set(reflect.ValueOf(p.parameter.Default)) } return nil } if err := consumer.Consume(request.Body, newValue.Interface()); err != nil { if stderrors.Is(err, io.EOF) && p.parameter.Default != nil { target.Set(reflect.ValueOf(p.parameter.Default)) return nil } tpe := p.parameter.Type if p.parameter.Format != "" { tpe = p.parameter.Format } return errors.InvalidType(p.Name, p.parameter.In, tpe, nil) } target.Set(reflect.Indirect(newValue)) return nil } func (p *untypedParamBinder) typeForSchema(tpe, format string, items *spec.Items) reflect.Type { switch tpe { case "boolean": return reflect.TypeFor[bool]() case typeString: if tt, ok := p.formats.GetType(format); ok { return tt } return reflect.TypeFor[string]() case "integer": switch format { case "int8": return reflect.TypeFor[int8]() case "int16": return reflect.TypeFor[int16]() case "int32": return reflect.TypeFor[int32]() case "int64": return reflect.TypeFor[int64]() default: return reflect.TypeFor[int64]() } case "number": switch format { case "float": return reflect.TypeFor[float32]() case "double": return reflect.TypeFor[float64]() } case typeArray: if items == nil { return nil } itemsType := p.typeForSchema(items.Type, items.Format, items.Items) if itemsType == nil { return nil } return reflect.MakeSlice(reflect.SliceOf(itemsType), 0, 0).Type() case "file": return reflect.TypeFor[runtime.File]() case "object": return reflect.TypeFor[map[string]any]() } return nil } func (p *untypedParamBinder) allowsMulti() bool { return p.parameter.In == "query" || p.parameter.In == "formData" } func (p *untypedParamBinder) readValue(values runtime.Gettable, target reflect.Value) ([]string, bool, bool, error) { name, in, cf, tpe := p.parameter.Name, p.parameter.In, p.parameter.CollectionFormat, p.parameter.Type if tpe == typeArray { if cf == "multi" { if !p.allowsMulti() { return nil, false, false, errors.InvalidCollectionFormat(name, in, cf) } vv, hasKey, _ := values.GetOK(name) return vv, false, hasKey, nil } v, hk, hv := values.GetOK(name) if !hv { return nil, false, hk, nil } d, c, e := p.readFormattedSliceFieldValue(v[len(v)-1], target) return d, c, hk, e } vv, hk, _ := values.GetOK(name) return vv, false, hk, nil } func (p *untypedParamBinder) bindValue(data []string, hasKey bool, target reflect.Value) error { if p.parameter.Type == typeArray { return p.setSliceFieldValue(target, p.parameter.Default, data, hasKey) } var d string if len(data) > 0 { d = data[len(data)-1] } return p.setFieldValue(target, p.parameter.Default, d, hasKey) } func (p *untypedParamBinder) isMissingAndRequired(hasKey bool, data string) bool { return p.parameter.Required && p.parameter.Default == nil && (!hasKey || (!p.parameter.AllowEmptyValue && data == "")) } func (p *untypedParamBinder) setByte(target, defVal reflect.Value, tpe, data string) error { if data == "" { if target.CanSet() { target.SetBytes(defVal.Bytes()) } return nil } b, err := base64.StdEncoding.DecodeString(data) if err != nil { b, err = base64.URLEncoding.DecodeString(data) if err != nil { return errors.InvalidType(p.Name, p.parameter.In, tpe, data) } } if target.CanSet() { target.SetBytes(b) } return nil } func (p *untypedParamBinder) setFieldValue(target reflect.Value, defaultValue any, data string, hasKey bool) error { tpe := p.parameter.Type if p.parameter.Format != "" { tpe = p.parameter.Format } if p.isMissingAndRequired(hasKey, data) { return errors.Required(p.Name, p.parameter.In, data) } ok, err := p.tryUnmarshaler(target, defaultValue, data) if err != nil { return errors.InvalidType(p.Name, p.parameter.In, tpe, data) } if ok { return nil } defVal := reflect.Zero(target.Type()) if defaultValue != nil { defVal = reflect.ValueOf(defaultValue) } if tpe == "byte" { return p.setByte(target, defVal, tpe, data) } return p.setReflectFieldValue(target, defVal, tpe, data, hasKey) } //nolint:gocyclo,cyclop // not much we can simplify further significantly: the big case with all types is unavoidable. func (p *untypedParamBinder) setReflectFieldValue(target, defVal reflect.Value, tpe, data string, hasKey bool) error { switch target.Kind() { // we want to check only types that map from a swagger parameter case reflect.Bool: if data == "" { if target.CanSet() { target.SetBool(defVal.Bool()) } return nil } b, err := conv.ConvertBool(data) if err != nil { return err } if target.CanSet() { target.SetBool(b) } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if data == "" { if target.CanSet() { rd := defVal.Convert(reflect.TypeFor[int64]()) target.SetInt(rd.Int()) } return nil } i, err := strconv.ParseInt(data, 10, 64) if err != nil { return errors.InvalidType(p.Name, p.parameter.In, tpe, data) } if target.OverflowInt(i) { return errors.InvalidType(p.Name, p.parameter.In, tpe, data) } if target.CanSet() { target.SetInt(i) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if data == "" { if target.CanSet() { rd := defVal.Convert(reflect.TypeFor[uint64]()) target.SetUint(rd.Uint()) } return nil } u, err := strconv.ParseUint(data, 10, 64) if err != nil { return errors.InvalidType(p.Name, p.parameter.In, tpe, data) } if target.OverflowUint(u) { return errors.InvalidType(p.Name, p.parameter.In, tpe, data) } if target.CanSet() { target.SetUint(u) } case reflect.Float32, reflect.Float64: if data == "" { if target.CanSet() { rd := defVal.Convert(reflect.TypeFor[float64]()) target.SetFloat(rd.Float()) } return nil } f, err := strconv.ParseFloat(data, 64) if err != nil { return errors.InvalidType(p.Name, p.parameter.In, tpe, data) } if target.OverflowFloat(f) { return errors.InvalidType(p.Name, p.parameter.In, tpe, data) } if target.CanSet() { target.SetFloat(f) } case reflect.String: value := data if value == "" { value = defVal.String() } // validate string if target.CanSet() { target.SetString(value) } case reflect.Pointer: if data == "" && defVal.Kind() == reflect.Pointer { if target.CanSet() { target.Set(defVal) } return nil } newVal := reflect.New(target.Type().Elem()) if err := p.setFieldValue(reflect.Indirect(newVal), defVal, data, hasKey); err != nil { return err } if target.CanSet() { target.Set(newVal) } default: return errors.InvalidType(p.Name, p.parameter.In, tpe, data) } return nil } func (p *untypedParamBinder) tryUnmarshaler(target reflect.Value, defaultValue any, data string) (bool, error) { if !target.CanSet() { return false, nil } // When a type implements encoding.TextUnmarshaler we'll use that instead of reflecting some more ttyp := target.Type() if !reflect.PointerTo(ttyp).Implements(textUnmarshalType) { return false, nil } if defaultValue != nil && len(data) == 0 { target.Set(reflect.ValueOf(defaultValue)) return true, nil } value := reflect.New(ttyp) if !value.CanInterface() { return false, nil } if err := value.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(data)); err != nil { //nolint:forcetypeassert // this is guaranteed by the reflect check above return true, err } target.Set(reflect.Indirect(value)) return true, nil } func (p *untypedParamBinder) readFormattedSliceFieldValue(data string, target reflect.Value) ([]string, bool, error) { ok, err := p.tryUnmarshaler(target, p.parameter.Default, data) if err != nil { return nil, true, err } if ok { return nil, true, nil } return stringutils.SplitByFormat(data, p.parameter.CollectionFormat), false, nil } func (p *untypedParamBinder) setSliceFieldValue(target reflect.Value, defaultValue any, data []string, hasKey bool) error { sz := len(data) if (!hasKey || (!p.parameter.AllowEmptyValue && (sz == 0 || (sz == 1 && data[0] == "")))) && p.parameter.Required && defaultValue == nil { return errors.Required(p.Name, p.parameter.In, data) } defVal := reflect.Zero(target.Type()) if defaultValue != nil { defVal = reflect.ValueOf(defaultValue) } if !target.CanSet() { return nil } if sz == 0 { target.Set(defVal) return nil } value := reflect.MakeSlice(reflect.SliceOf(target.Type().Elem()), sz, sz) for i := range sz { if err := p.setFieldValue(value.Index(i), nil, data[i], hasKey); err != nil { return err } } target.Set(value) return nil } go-openapi-runtime-decad8f/middleware/parameter_test.go000066400000000000000000000264301520232310000235450ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "math" "net/url" "reflect" "strconv" "testing" "github.com/go-openapi/errors" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" "github.com/go-openapi/runtime" ) type paramFactory func(string) *spec.Parameter var paramFactories = []paramFactory{ spec.QueryParam, spec.HeaderParam, spec.PathParam, spec.FormDataParam, } func np(param *spec.Parameter) *untypedParamBinder { return newUntypedParamBinder(*param, new(spec.Swagger), strfmt.Default) } var stringItems = new(spec.Items) func init() { stringItems.Type = typeString } func testCollectionFormat(t *testing.T, param *spec.Parameter, valid bool) { t.Helper() binder := &untypedParamBinder{ parameter: param, } _, _, _, err := binder.readValue(runtime.Values(nil), reflect.ValueOf(nil)) //nolint:dogsled // we just want to test the error if valid { require.NoError(t, err) } else { require.Error(t, err) require.EqualError(t, err, errors.InvalidCollectionFormat(param.Name, param.In, param.CollectionFormat).Error()) } } func requiredError(param *spec.Parameter, data any) *errors.Validation { return errors.Required(param.Name, param.In, data) } func validateRequiredTest(t *testing.T, param *spec.Parameter, value reflect.Value) { binder := np(param) err := binder.bindValue([]string{}, true, value) require.Error(t, err) assert.NotNil(t, param) require.EqualError(t, requiredError(param, value.Interface()), err.Error()) err = binder.bindValue([]string{""}, true, value) require.Error(t, err) require.EqualError(t, requiredError(param, value.Interface()), err.Error()) // should be impossible data, but let's go with it err = binder.bindValue([]string{"a"}, false, value) require.Error(t, err) require.EqualError(t, err, requiredError(param, value.Interface()).Error()) err = binder.bindValue([]string{""}, false, value) require.Error(t, err) require.EqualError(t, requiredError(param, value.Interface()), err.Error()) } func validateRequiredAllowEmptyTest(t *testing.T, param *spec.Parameter, value reflect.Value) { param.AllowEmptyValue = true binder := np(param) err := binder.bindValue([]string{}, true, value) require.NoError(t, err) require.NotNil(t, param) err = binder.bindValue([]string{""}, true, value) require.NoError(t, err) err = binder.bindValue([]string{"1"}, false, value) require.Error(t, err) require.EqualError(t, requiredError(param, value.Interface()), err.Error()) err = binder.bindValue([]string{""}, false, value) require.Error(t, err) require.EqualError(t, requiredError(param, value.Interface()), err.Error()) } func TestRequiredValidation(t *testing.T) { strParam := spec.QueryParam("name").Typed(typeString, "").AsRequired() validateRequiredTest(t, strParam, reflect.ValueOf("")) validateRequiredAllowEmptyTest(t, strParam, reflect.ValueOf("")) intParam := spec.QueryParam(paramKeyID).Typed("integer", "int32").AsRequired() validateRequiredTest(t, intParam, reflect.ValueOf(int32(0))) validateRequiredAllowEmptyTest(t, intParam, reflect.ValueOf(int32(0))) longParam := spec.QueryParam(paramKeyID).Typed("integer", "int64").AsRequired() validateRequiredTest(t, longParam, reflect.ValueOf(int64(0))) validateRequiredAllowEmptyTest(t, longParam, reflect.ValueOf(int64(0))) floatParam := spec.QueryParam("score").Typed("number", "float").AsRequired() validateRequiredTest(t, floatParam, reflect.ValueOf(float32(0))) validateRequiredAllowEmptyTest(t, floatParam, reflect.ValueOf(float32(0))) doubleParam := spec.QueryParam("score").Typed("number", "double").AsRequired() validateRequiredTest(t, doubleParam, reflect.ValueOf(float64(0))) validateRequiredAllowEmptyTest(t, doubleParam, reflect.ValueOf(float64(0))) dateTimeParam := spec.QueryParam("registered").Typed(typeString, "date-time").AsRequired() validateRequiredTest(t, dateTimeParam, reflect.ValueOf(strfmt.DateTime{})) // validateRequiredAllowEmptyTest(t, dateTimeParam, reflect.ValueOf(strfmt.DateTime{})) dateParam := spec.QueryParam("registered").Typed(typeString, "date").AsRequired() validateRequiredTest(t, dateParam, reflect.ValueOf(strfmt.Date{})) // validateRequiredAllowEmptyTest(t, dateParam, reflect.ValueOf(strfmt.DateTime{})) sliceParam := spec.QueryParam("tags").CollectionOf(stringItems, "").AsRequired() validateRequiredTest(t, sliceParam, reflect.MakeSlice(reflect.TypeFor[[]string](), 0, 0)) validateRequiredAllowEmptyTest(t, sliceParam, reflect.MakeSlice(reflect.TypeFor[[]string](), 0, 0)) } func TestInvalidCollectionFormat(t *testing.T) { validCf1 := spec.QueryParam("validFmt").CollectionOf(stringItems, multiFmt) validCf2 := spec.FormDataParam("validFmt2").CollectionOf(stringItems, multiFmt) invalidCf1 := spec.HeaderParam("invalidHdr").CollectionOf(stringItems, multiFmt) invalidCf2 := spec.PathParam("invalidPath").CollectionOf(stringItems, multiFmt) testCollectionFormat(t, validCf1, true) testCollectionFormat(t, validCf2, true) testCollectionFormat(t, invalidCf1, false) testCollectionFormat(t, invalidCf2, false) } func invalidTypeError(param *spec.Parameter, data any) *errors.Validation { tpe := param.Type if param.Format != "" { tpe = param.Format } return errors.InvalidType(param.Name, param.In, tpe, data) } func TestTypeValidation(t *testing.T) { for _, newParam := range paramFactories { intParam := newParam("badInt").Typed("integer", "int32") value := reflect.ValueOf(int32(0)) binder := np(intParam) err := binder.bindValue([]string{valYada}, true, value) // fails for invalid string require.Error(t, err) require.EqualError(t, err, invalidTypeError(intParam, valYada).Error()) // fails for overflow val := int64(math.MaxInt32) str := strconv.FormatInt(val, 10) + "0" v := int32(0) value = reflect.ValueOf(&v).Elem() binder = np(intParam) err = binder.bindValue([]string{str}, true, value) require.Error(t, err) require.EqualError(t, err, invalidTypeError(intParam, str).Error()) // fails for invalid string longParam := newParam("badLong").Typed("integer", "int64") value = reflect.ValueOf(int64(0)) binder = np(longParam) err = binder.bindValue([]string{valYada}, true, value) require.Error(t, err) require.EqualError(t, err, invalidTypeError(longParam, valYada).Error()) // fails for overflow str2 := strconv.FormatInt(math.MaxInt64, 10) + "0" v2 := int64(0) vv2 := reflect.ValueOf(&v2).Elem() binder = np(longParam) err = binder.bindValue([]string{str2}, true, vv2) require.Error(t, err) require.EqualError(t, err, invalidTypeError(longParam, str2).Error()) // fails for invalid string floatParam := newParam("badFloat").Typed("number", "float") value = reflect.ValueOf(float64(0)) binder = np(floatParam) err = binder.bindValue([]string{valYada}, true, value) require.Error(t, err) require.EqualError(t, err, invalidTypeError(floatParam, valYada).Error()) // fails for overflow str3 := strconv.FormatFloat(math.MaxFloat64, 'f', 5, 64) v3 := reflect.TypeFor[float32]() value = reflect.New(v3).Elem() binder = np(floatParam) err = binder.bindValue([]string{str3}, true, value) require.Error(t, err) require.EqualError(t, err, invalidTypeError(floatParam, str3).Error()) // fails for invalid string doubleParam := newParam("badDouble").Typed("number", "double") value = reflect.ValueOf(float64(0)) binder = np(doubleParam) err = binder.bindValue([]string{valYada}, true, value) require.Error(t, err) require.EqualError(t, err, invalidTypeError(doubleParam, valYada).Error()) // fails for overflow str4 := "9" + strconv.FormatFloat(math.MaxFloat64, 'f', 5, 64) v4 := reflect.TypeFor[float64]() value = reflect.New(v4).Elem() binder = np(doubleParam) err = binder.bindValue([]string{str4}, true, value) require.Error(t, err) require.EqualError(t, err, invalidTypeError(doubleParam, str4).Error()) // fails for invalid string dateParam := newParam("badDate").Typed(typeString, "date") value = reflect.ValueOf(strfmt.Date{}) binder = np(dateParam) err = binder.bindValue([]string{valYada}, true, value) require.Error(t, err) require.EqualError(t, err, invalidTypeError(dateParam, valYada).Error()) // fails for invalid string dateTimeParam := newParam("badDateTime").Typed(typeString, "date-time") value = reflect.ValueOf(strfmt.DateTime{}) binder = np(dateTimeParam) err = binder.bindValue([]string{valYada}, true, value) require.Error(t, err) require.EqualError(t, err, invalidTypeError(dateTimeParam, valYada).Error()) // fails for invalid string byteParam := newParam("badByte").Typed(typeString, "byte") values := url.Values(map[string][]string{}) values.Add("badByte", "yaüda") v5 := []byte{} value = reflect.ValueOf(&v5).Elem() binder = np(byteParam) err = binder.bindValue([]string{"yaüda"}, true, value) require.Error(t, err) require.EqualError(t, err, invalidTypeError(byteParam, "yaüda").Error()) } } func TestTypeDetectionInvalidItems(t *testing.T) { withoutItems := spec.QueryParam("without").CollectionOf(nil, "") binder := &untypedParamBinder{ Name: "without", parameter: withoutItems, } assert.Nil(t, binder.Type()) items := new(spec.Items) items.Type = typeArray withInvalidItems := spec.QueryParam("invalidItems").CollectionOf(items, "") binder = &untypedParamBinder{ Name: "invalidItems", parameter: withInvalidItems, } assert.Nil(t, binder.Type()) noType := spec.QueryParam("invalidType") noType.Type = "invalid" binder = &untypedParamBinder{ Name: "invalidType", parameter: noType, } assert.Nil(t, binder.Type()) } // type emailStrFmt struct { // name string // tpe reflect.Type // validator FormatValidator // } // // func (e *emailStrFmt) Name() string { // return e.name // } // // func (e *emailStrFmt) Type() reflect.Type { // return e.tpe // } // // func (e *emailStrFmt) Matches(str string) bool { // return e.validator(str) // } // // func TestTypeDetectionValid(t *testing.T) { // // emlFmt := &emailStrFmt{ // // name: "email", // // tpe: reflect.TypeOf(email{}), // // } // // formats := []StringFormat{emlFmt} // // expected := map[string]reflect.Type{ // "name": reflect.TypeOf(""), // "id": reflect.TypeOf(int64(0)), // "age": reflect.TypeOf(int32(0)), // "score": reflect.TypeOf(float32(0)), // "factor": reflect.TypeOf(float64(0)), // "friend": reflect.TypeOf(map[string]interface{}{}), // "X-Request-Id": reflect.TypeOf(int64(0)), // "tags": reflect.TypeOf([]string{}), // "confirmed": reflect.TypeOf(true), // "planned": reflect.TypeOf(swagger.Date{}), // "delivered": reflect.TypeOf(swagger.DateTime{}), // "email": reflect.TypeOf(email{}), // "picture": reflect.TypeOf([]byte{}), // "file": reflect.TypeOf(&swagger.File{}).Elem(), // } // // params := parametersForAllTypes("") // emailParam := spec.QueryParam("email").Typed(typeString, "email") // params["email"] = *emailParam // // fileParam := spec.FileParam("file") // params["file"] = *fileParam // // for _, v := range params { // binder := ¶mBinder{ // formats: formats, // name: v.Name, // parameter: &v, // } // assert.Equal(t, expected[v.Name], binder.Type(), "name: %s", v.Name) // } // } go-openapi-runtime-decad8f/middleware/request.go000066400000000000000000000065221520232310000222160ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "net/http" "reflect" "github.com/go-openapi/errors" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/logger" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" ) // UntypedRequestBinder binds and validates the data from a [http] request. type UntypedRequestBinder struct { Spec *spec.Swagger Parameters map[string]spec.Parameter Formats strfmt.Registry paramBinders map[string]*untypedParamBinder debugLogf func(string, ...any) // a logging function to debug context and all components using it } // NewUntypedRequestBinder creates a new binder for reading a request. func NewUntypedRequestBinder(parameters map[string]spec.Parameter, spec *spec.Swagger, formats strfmt.Registry) *UntypedRequestBinder { binders := make(map[string]*untypedParamBinder) for fieldName, param := range parameters { binders[fieldName] = newUntypedParamBinder(param, spec, formats) } return &UntypedRequestBinder{ Parameters: parameters, paramBinders: binders, Spec: spec, Formats: formats, debugLogf: debugLogfFunc(nil), } } // Bind perform the databinding and validation. func (o *UntypedRequestBinder) Bind(request *http.Request, routeParams RouteParams, consumer runtime.Consumer, data any) error { err := o.bind(request, routeParams, consumer, data) if err == nil { return nil // avoids returning a nil-interface } return err } // SetLogger allows for injecting a logger to catch debug entries. // // The logger is enabled in DEBUG mode only. func (o *UntypedRequestBinder) SetLogger(lg logger.Logger) { o.debugLogf = debugLogfFunc(lg) } func (o *UntypedRequestBinder) bind(request *http.Request, routeParams RouteParams, consumer runtime.Consumer, data any) *errors.CompositeError { val := reflect.Indirect(reflect.ValueOf(data)) isMap := val.Kind() == reflect.Map var result []error o.debugLogf("binding %d parameters for %s %s", len(o.Parameters), request.Method, request.URL.EscapedPath()) for fieldName, param := range o.Parameters { binder := o.paramBinders[fieldName] o.debugLogf("binding parameter %s for %s %s", fieldName, request.Method, request.URL.EscapedPath()) var target reflect.Value if !isMap { binder.Name = fieldName target = val.FieldByName(fieldName) } if isMap { tpe := binder.Type() if tpe == nil { if param.Schema.Type.Contains(typeArray) { tpe = reflect.TypeFor[[]any]() } else { tpe = reflect.TypeFor[map[string]any]() } } target = reflect.Indirect(reflect.New(tpe)) } if !target.IsValid() { result = append(result, errors.New(http.StatusInternalServerError, "parameter name %q is an unknown field", binder.Name)) continue } if err := binder.Bind(request, routeParams, consumer, target); err != nil { result = append(result, err) continue } if binder.validator != nil { rr := binder.validator.Validate(target.Interface()) if rr != nil && rr.HasErrors() { result = append(result, rr.AsError()) } } if isMap { val.SetMapIndex(reflect.ValueOf(param.Name), target) } } if len(result) > 0 { return errors.CompositeValidationError(result...) } return nil } func (o *UntypedRequestBinder) setDebugLogf(fn func(string, ...any)) { o.debugLogf = fn } go-openapi-runtime-decad8f/middleware/request_test.go000066400000000000000000000501301520232310000232470ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "bytes" "context" "io" "mime/multipart" "net/http" "net/url" "strings" "testing" "time" "github.com/go-openapi/runtime" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) const ( csvFormat = "csv" testURL = "http://localhost:8002/hello" ) type stubConsumer struct { } func (s *stubConsumer) Consume(_ io.Reader, _ any) error { return nil } type friend struct { Name string `json:"name"` Age int `json:"age"` } type jsonRequestParams struct { ID int64 // path Name string // query Friend friend // body RequestID int64 // header Tags []string // csv } type jsonRequestPtr struct { ID int64 // path Name string // query RequestID int64 // header Tags []string // csv Friend *friend } type jsonRequestSlice struct { ID int64 // path Name string // query RequestID int64 // header Tags []string // csv Friend []friend } func parametersForAllTypes(fmt string) map[string]spec.Parameter { if fmt == "" { fmt = csvFormat } nameParam := spec.QueryParam(paramKeyName).Typed(typeString, "") idParam := spec.PathParam(paramKeyID).Typed("integer", "int64") ageParam := spec.QueryParam(paramKeyAge).Typed("integer", "int32") scoreParam := spec.QueryParam("score").Typed("number", "float") factorParam := spec.QueryParam("factor").Typed("number", "double") friendSchema := new(spec.Schema).Typed("object", "") friendParam := spec.BodyParam("friend", friendSchema) requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64") requestIDParam.Extensions = spec.Extensions(map[string]any{}) requestIDParam.Extensions.Add("go-name", keyRequestID) items := new(spec.Items) items.Type = typeString tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt) confirmedParam := spec.QueryParam("confirmed").Typed("boolean", "") plannedParam := spec.QueryParam("planned").Typed(typeString, "date") deliveredParam := spec.QueryParam("delivered").Typed(typeString, "date-time") pictureParam := spec.QueryParam("picture").Typed(typeString, "byte") // base64 encoded during transport return map[string]spec.Parameter{ keyID: *idParam, keyName: *nameParam, keyRequestID: *requestIDParam, keyFriend: *friendParam, keyTags: *tagsParam, "Age": *ageParam, "Score": *scoreParam, "Factor": *factorParam, "Confirmed": *confirmedParam, "Planned": *plannedParam, "Delivered": *deliveredParam, "Picture": *pictureParam, } } func parametersForJSONRequestParams(fmt string) map[string]spec.Parameter { if fmt == "" { fmt = csvFormat } nameParam := spec.QueryParam(paramKeyName).Typed(typeString, "") idParam := spec.PathParam(paramKeyID).Typed("integer", "int64") friendSchema := new(spec.Schema).Typed("object", "") friendParam := spec.BodyParam("friend", friendSchema) requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64") requestIDParam.Extensions = spec.Extensions(map[string]any{}) requestIDParam.Extensions.Add("go-name", keyRequestID) items := new(spec.Items) items.Type = typeString tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt) return map[string]spec.Parameter{ keyID: *idParam, keyName: *nameParam, keyRequestID: *requestIDParam, keyFriend: *friendParam, keyTags: *tagsParam, } } func parametersForJSONRequestSliceParams(fmt string) map[string]spec.Parameter { if fmt == "" { fmt = csvFormat } nameParam := spec.QueryParam(paramKeyName).Typed(typeString, "") idParam := spec.PathParam(paramKeyID).Typed("integer", "int64") friendSchema := new(spec.Schema).Typed("object", "") friendParam := spec.BodyParam("friend", spec.ArrayProperty(friendSchema)) requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64") requestIDParam.Extensions = spec.Extensions(map[string]any{}) requestIDParam.Extensions.Add("go-name", keyRequestID) items := new(spec.Items) items.Type = typeString tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt) return map[string]spec.Parameter{ keyID: *idParam, keyName: *nameParam, keyRequestID: *requestIDParam, keyFriend: *friendParam, keyTags: *tagsParam, } } func TestRequestBindingDefaultValue(t *testing.T) { confirmed := true name := "thomas" friend := map[string]any{paramKeyName: valToby, paramKeyAge: float64(32)} id, age, score, factor := int64(7575), int32(348), float32(5.309), float64(37.403) requestID := 19394858 tags := []string{tagOne, tagTwo, tagThree} dt1 := time.Date(2014, 8, 9, 0, 0, 0, 0, time.UTC) planned := strfmt.Date(dt1) dt2 := time.Date(2014, 10, 12, 8, 5, 5, 0, time.UTC) delivered := strfmt.DateTime(dt2) uri, err := url.Parse(testURL) require.NoError(t, err) defaults := map[string]any{ paramKeyID: id, paramKeyAge: age, "score": score, "factor": factor, paramKeyName: name, "friend": friend, "X-Request-Id": requestID, "tags": tags, "confirmed": confirmed, "planned": planned, "delivered": delivered, "picture": []byte("hello"), } op2 := parametersForAllTypes("") op3 := make(map[string]spec.Parameter) for k, p := range op2 { p.Default = defaults[p.Name] op3[k] = p } req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, uri.String(), bytes.NewBuffer(nil)) require.NoError(t, err) req.Header.Set("Content-Type", jsonMime) binder := NewUntypedRequestBinder(op3, new(spec.Swagger), strfmt.Default) data := make(map[string]any) err = binder.Bind(req, RouteParams(nil), runtime.JSONConsumer(), &data) require.NoError(t, err) assert.Equal(t, defaults[paramKeyID], data[paramKeyID]) assert.Equal(t, name, data[paramKeyName]) assert.Equal(t, friend, data["friend"]) assert.EqualValues(t, requestID, data["X-Request-Id"]) assert.Equal(t, tags, data["tags"]) assert.Equal(t, planned, data["planned"]) assert.Equal(t, delivered, data["delivered"]) assert.Equal(t, confirmed, data["confirmed"]) assert.Equal(t, age, data[paramKeyAge]) assert.InDelta(t, factor, data["factor"], 1e-6) assert.InDelta(t, score, data["score"], 1e-6) formatted, ok := data["picture"].(strfmt.Base64) require.TrueT(t, ok) assert.EqualT(t, "hello", string(formatted)) } func TestRequestBindingForInvalid(t *testing.T) { invalidParam := spec.QueryParam("some") op1 := map[string]spec.Parameter{"Some": *invalidParam} binder := NewUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost:8002/hello?name=the-name", nil) require.NoError(t, err) err = binder.Bind(req, nil, new(stubConsumer), new(jsonRequestParams)) require.Error(t, err) op2 := parametersForJSONRequestParams("") binder = NewUntypedRequestBinder(op2, new(spec.Swagger), strfmt.Default) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost:8002/hello/1?name=the-name", bytes.NewBufferString(`{"name":"toby","age":32}`)) require.NoError(t, err) req.Header.Set("Content-Type", "application(") data := jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{paramKeyID, "1"}}), runtime.JSONConsumer(), &data) require.Error(t, err) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost:8002/hello/1?name=the-name", bytes.NewBufferString(`{]`)) require.NoError(t, err) req.Header.Set("Content-Type", jsonMime) data = jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{paramKeyID, "1"}}), runtime.JSONConsumer(), &data) require.Error(t, err) invalidMultiParam := spec.HeaderParam("tags").CollectionOf(new(spec.Items), multiFmt) op3 := map[string]spec.Parameter{keyTags: *invalidMultiParam} binder = NewUntypedRequestBinder(op3, new(spec.Swagger), strfmt.Default) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost:8002/hello/1?name=the-name", bytes.NewBufferString(`{}`)) require.NoError(t, err) req.Header.Set("Content-Type", jsonMime) data = jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{paramKeyID, "1"}}), runtime.JSONConsumer(), &data) require.Error(t, err) invalidMultiParam = spec.PathParam("").CollectionOf(new(spec.Items), multiFmt) op4 := map[string]spec.Parameter{keyTags: *invalidMultiParam} binder = NewUntypedRequestBinder(op4, new(spec.Swagger), strfmt.Default) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost:8002/hello/1?name=the-name", bytes.NewBufferString(`{}`)) require.NoError(t, err) req.Header.Set("Content-Type", jsonMime) data = jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{paramKeyID, "1"}}), runtime.JSONConsumer(), &data) require.Error(t, err) invalidInParam := spec.HeaderParam("tags").Typed(typeString, "") invalidInParam.In = "invalid" op5 := map[string]spec.Parameter{keyTags: *invalidInParam} binder = NewUntypedRequestBinder(op5, new(spec.Swagger), strfmt.Default) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost:8002/hello/1?name=the-name", bytes.NewBufferString(`{}`)) require.NoError(t, err) req.Header.Set("Content-Type", jsonMime) data = jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{paramKeyID, "1"}}), runtime.JSONConsumer(), &data) require.Error(t, err) } func TestRequestBindingForValid(t *testing.T) { for _, fmt := range []string{csvFormat, pipesFmt, tsvFmt, ssvFmt, multiFmt} { op1 := parametersForJSONRequestParams(fmt) binder := NewUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default) lval := []string{tagOne, tagTwo, tagThree} var queryString string var skipEscape bool switch fmt { case multiFmt: skipEscape = true queryString = strings.Join(lval, "&tags=") case ssvFmt: queryString = strings.Join(lval, " ") case pipesFmt: queryString = strings.Join(lval, "|") case tsvFmt: queryString = strings.Join(lval, "\t") default: queryString = strings.Join(lval, ",") } if !skipEscape { queryString = url.QueryEscape(queryString) } urlStr := "http://localhost:8002/hello/1?name=the-name&tags=" + queryString req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, bytes.NewBufferString(`{"name":"toby","age":32}`)) require.NoError(t, err) req.Header.Set("Content-Type", "application/json;charset=utf-8") req.Header.Set("X-Request-Id", "1325959595") data := jsonRequestParams{} err = binder.Bind(req, RouteParams([]RouteParam{{paramKeyID, "1"}}), runtime.JSONConsumer(), &data) expected := jsonRequestParams{ ID: 1, Name: "the-name", Friend: friend{valToby, 32}, RequestID: 1325959595, Tags: []string{tagOne, tagTwo, tagThree}, } require.NoError(t, err) assert.Equal(t, expected, data) } op1 := parametersForJSONRequestParams("") binder := NewUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default) urlStr := "http://localhost:8002/hello/1?name=the-name&tags=one,two,three" req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, bytes.NewBufferString(`{"name":"toby","age":32}`)) require.NoError(t, err) req.Header.Set("Content-Type", "application/json;charset=utf-8") req.Header.Set("X-Request-Id", "1325959595") data2 := jsonRequestPtr{} err = binder.Bind(req, []RouteParam{{paramKeyID, "1"}}, runtime.JSONConsumer(), &data2) expected2 := jsonRequestPtr{ Friend: &friend{valToby, 32}, Tags: []string{tagOne, tagTwo, tagThree}, } require.NoError(t, err) if data2.Friend == nil { t.Fatal("friend is nil") } assert.EqualT(t, *expected2.Friend, *data2.Friend) assert.Equal(t, expected2.Tags, data2.Tags) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, bytes.NewBufferString(`[{"name":"toby","age":32}]`)) require.NoError(t, err) req.Header.Set("Content-Type", "application/json;charset=utf-8") req.Header.Set("X-Request-Id", "1325959595") op2 := parametersForJSONRequestSliceParams("") binder = NewUntypedRequestBinder(op2, new(spec.Swagger), strfmt.Default) data3 := jsonRequestSlice{} err = binder.Bind(req, []RouteParam{{paramKeyID, "1"}}, runtime.JSONConsumer(), &data3) expected3 := jsonRequestSlice{ Friend: []friend{{valToby, 32}}, Tags: []string{tagOne, tagTwo, tagThree}, } require.NoError(t, err) assert.Equal(t, expected3.Friend, data3.Friend) assert.Equal(t, expected3.Tags, data3.Tags) } type formRequest struct { Name string Age int } func parametersForFormUpload() map[string]spec.Parameter { nameParam := spec.FormDataParam(paramKeyName).Typed(typeString, "") ageParam := spec.FormDataParam(paramKeyAge).Typed("integer", "int32") return map[string]spec.Parameter{keyName: *nameParam, "Age": *ageParam} } func TestFormUpload(t *testing.T) { params := parametersForFormUpload() binder := NewUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default) urlStr := testURL req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, bytes.NewBufferString(`name=the-name&age=32`)) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") data := formRequest{} res := binder.Bind(req, nil, runtime.JSONConsumer(), &data) require.NoError(t, res) assert.EqualT(t, "the-name", data.Name) assert.EqualT(t, 32, data.Age) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, bytes.NewBufferString(`name=%3&age=32`)) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") data = formRequest{} require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) } type fileRequest struct { Name string // body File runtime.File // upload } func paramsForFileUpload() *UntypedRequestBinder { nameParam := spec.FormDataParam(paramKeyName).Typed(typeString, "") fileParam := spec.FileParam("file").AsRequired() params := map[string]spec.Parameter{keyName: *nameParam, "File": *fileParam} return NewUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default) } func TestBindingFileUpload(t *testing.T) { binder := paramsForFileUpload() body := bytes.NewBuffer(nil) writer := multipart.NewWriter(body) part, err := writer.CreateFormFile("file", "plain-jane.txt") require.NoError(t, err) _, err = part.Write([]byte("the file contents")) require.NoError(t, err) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) urlStr := testURL req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) data := fileRequest{} require.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) assert.EqualT(t, "the-name", data.Name) assert.NotNil(t, data.File) assert.NotNil(t, data.File.Header) assert.EqualT(t, "plain-jane.txt", data.File.Header.Filename) bb, err := io.ReadAll(data.File.Data) require.NoError(t, err) assert.Equal(t, []byte("the file contents"), bb) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body) require.NoError(t, err) req.Header.Set("Content-Type", jsonMime) data = fileRequest{} require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body) require.NoError(t, err) req.Header.Set("Content-Type", "application(") data = fileRequest{} require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) body = bytes.NewBuffer(nil) writer = multipart.NewWriter(body) part, err = writer.CreateFormFile("bad-name", "plain-jane.txt") require.NoError(t, err) _, err = part.Write([]byte("the file contents")) require.NoError(t, err) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) data = fileRequest{} require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) _, err = req.MultipartReader() require.NoError(t, err) data = fileRequest{} require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) writer = multipart.NewWriter(body) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) data = fileRequest{} require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) } func paramsForOptionalFileUpload() *UntypedRequestBinder { nameParam := spec.FormDataParam(paramKeyName).Typed(typeString, "") fileParam := spec.FileParam("file").AsOptional() params := map[string]spec.Parameter{keyName: *nameParam, "File": *fileParam} return NewUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default) } func TestBindingOptionalFileUpload(t *testing.T) { binder := paramsForOptionalFileUpload() body := bytes.NewBuffer(nil) writer := multipart.NewWriter(body) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) urlStr := testURL req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) data := fileRequest{} require.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) assert.EqualT(t, "the-name", data.Name) assert.Nil(t, data.File.Data) assert.Nil(t, data.File.Header) writer = multipart.NewWriter(body) part, err := writer.CreateFormFile("file", "plain-jane.txt") require.NoError(t, err) _, err = part.Write([]byte("the file contents")) require.NoError(t, err) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, urlStr, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) require.NoError(t, writer.Close()) data = fileRequest{} require.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) assert.EqualT(t, "the-name", data.Name) assert.NotNil(t, data.File) assert.NotNil(t, data.File.Header) assert.EqualT(t, "plain-jane.txt", data.File.Header.Filename) bb, err := io.ReadAll(data.File.Data) require.NoError(t, err) assert.Equal(t, []byte("the file contents"), bb) } // TestBindingFileUpload_RejectsOversizedFilename exercises the // filename-length cap on the untyped formData path: a multipart // body with a multi-MB filename must be rejected with a ParseError // before the file is bound. // // Mirrors the BindFormFile-path coverage in // runtime.TestBindForm_maxFilenameLen_exceeded. Security scrub // Lens 3 / L3.1. func TestBindingFileUpload_RejectsOversizedFilename(t *testing.T) { binder := paramsForFileUpload() body := bytes.NewBuffer(nil) writer := multipart.NewWriter(body) longName := strings.Repeat("x", runtime.DefaultMaxUploadFilenameLength+1) + ".txt" part, err := writer.CreateFormFile("file", longName) require.NoError(t, err) _, err = part.Write([]byte("contents")) require.NoError(t, err) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) data := fileRequest{} bindErr := binder.Bind(req, nil, runtime.JSONConsumer(), &data) require.Error(t, bindErr) assert.Contains(t, bindErr.Error(), "exceeds limit") // File must NOT have been bound past the cap. assert.Nil(t, data.File.Data) assert.Nil(t, data.File.Header) } go-openapi-runtime-decad8f/middleware/route_authenticator_test.go000066400000000000000000000277641520232310000256700ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "context" "errors" "net/http" "testing" "github.com/go-openapi/testify/v2/require" "github.com/go-openapi/runtime" ) const testAuthenticator = "auth1" type principalType struct { Name string } type countAuthenticator struct { count int applies bool principal any err error } func (c *countAuthenticator) Authenticate(_ any) (bool, any, error) { c.count++ return c.applies, c.principal, c.err } func newCountAuthenticator(applies bool, principal any, err error) *countAuthenticator { return &countAuthenticator{applies: applies, principal: principal, err: err} } var ( successAuth = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) { return true, valTheUser, nil }) failAuth = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) { return true, nil, errors.New("unauthenticated") }) noApplyAuth = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) { return false, nil, nil }) successAuthWithPointer = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) { return true, &principalType{Name: valTheUser}, nil }) failAuthWithPointer = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) { var typedPrincipal *principalType return true, typedPrincipal, errors.New("unauthenticated") }) failAuthWithNilPointer = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) { var typedPrincipal *principalType return true, typedPrincipal, nil }) ) func TestAuthenticateSingle(t *testing.T) { t.Parallel() t.Run("with string", func(t *testing.T) { t.Parallel() ra := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuthenticator: successAuth, }, Schemes: []string{testAuthenticator}, Scopes: map[string][]string{testAuthenticator: nil}, } ras := RouteAuthenticators([]RouteAuthenticator{ra}) require.FalseT(t, ras.AllowsAnonymous()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route := &MatchedRoute{} ok, prin, err := ras.Authenticate(req, route) require.NoError(t, err) require.TrueT(t, ok) require.Equal(t, valTheUser, prin) require.Equal(t, ra, *route.Authenticator) }) t.Run("with pointer", func(t *testing.T) { t.Parallel() ra := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuthenticator: successAuthWithPointer, }, Schemes: []string{testAuthenticator}, Scopes: map[string][]string{testAuthenticator: nil}, } ras := RouteAuthenticators([]RouteAuthenticator{ra}) require.FalseT(t, ras.AllowsAnonymous()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route := &MatchedRoute{} ok, prin, err := ras.Authenticate(req, route) require.NoError(t, err) require.TrueT(t, ok) typed, ok := prin.(*principalType) require.TrueT(t, ok) require.EqualT(t, valTheUser, typed.Name) require.Equal(t, ra, *route.Authenticator) }) } func TestAuthenticateWrong(t *testing.T) { t.Run("with principal as a pointer to a concrete type", func(t *testing.T) { t.Run("should authenticate", func(t *testing.T) { ra := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuthenticator: successAuthWithPointer, }, Schemes: []string{testAuthenticator}, Scopes: map[string][]string{testAuthenticator: nil}, } ras := RouteAuthenticators([]RouteAuthenticator{ra}) require.False(t, ras.AllowsAnonymous()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route := &MatchedRoute{} ok, prin, err := ras.Authenticate(req, route) require.NoError(t, err) require.True(t, ok) require.EqualValues(t, &principalType{Name: valTheUser}, prin) }) t.Run("should not authenticate", func(t *testing.T) { ra := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuthenticator: failAuthWithPointer, }, Schemes: []string{testAuthenticator}, Scopes: map[string][]string{testAuthenticator: nil}, } ras := RouteAuthenticators([]RouteAuthenticator{ra}) require.False(t, ras.AllowsAnonymous()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route := &MatchedRoute{} ok, prin, err := ras.Authenticate(req, route) require.Error(t, err) require.True(t, ok) require.Nil(t, prin) }) t.Run("should yield nil principal", func(t *testing.T) { ra := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuthenticator: failAuthWithNilPointer, }, Schemes: []string{testAuthenticator}, Scopes: map[string][]string{testAuthenticator: nil}, } ras := RouteAuthenticators([]RouteAuthenticator{ra}) require.False(t, ras.AllowsAnonymous()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route := &MatchedRoute{} ok, prin, err := ras.Authenticate(req, route) require.NoError(t, err) require.FalseT(t, ok, "should not be authenticated: the principal is nil") require.Nil(t, prin) }) }) } func TestAuthenticateLogicalOr(t *testing.T) { ra1 := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuthenticator: noApplyAuth, }, Schemes: []string{testAuthenticator}, Scopes: map[string][]string{testAuthenticator: nil}, } ra2 := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuth2: successAuth, }, Schemes: []string{testAuth2}, Scopes: map[string][]string{testAuth2: nil}, } // right side matches ras := RouteAuthenticators([]RouteAuthenticator{ra1, ra2}) require.FalseT(t, ras.AllowsAnonymous()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route := &MatchedRoute{} ok, prin, err := ras.Authenticate(req, route) require.NoError(t, err) require.TrueT(t, ok) require.Equal(t, valTheUser, prin) require.Equal(t, ra2, *route.Authenticator) // left side matches ras = RouteAuthenticators([]RouteAuthenticator{ra2, ra1}) require.FalseT(t, ras.AllowsAnonymous()) req, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route = &MatchedRoute{} ok, prin, err = ras.Authenticate(req, route) require.NoError(t, err) require.TrueT(t, ok) require.Equal(t, valTheUser, prin) require.Equal(t, ra2, *route.Authenticator) } func TestAuthenticateLogicalAnd(t *testing.T) { ra1 := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuthenticator: noApplyAuth, }, Schemes: []string{testAuthenticator}, Scopes: map[string][]string{testAuthenticator: nil}, } authorizer := newCountAuthenticator(true, valTheUser, nil) ra2 := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuth2: authorizer, testAuth3: authorizer, }, Schemes: []string{testAuth2, testAuth3}, Scopes: map[string][]string{testAuth2: nil}, } ras := RouteAuthenticators([]RouteAuthenticator{ra1, ra2}) require.FalseT(t, ras.AllowsAnonymous()) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) require.NoError(t, err) route := &MatchedRoute{} ok, prin, err := ras.Authenticate(req, route) require.NoError(t, err) require.TrueT(t, ok) require.Equal(t, valTheUser, prin) require.Equal(t, ra2, *route.Authenticator) require.EqualT(t, 2, authorizer.count) var count int successA := runtime.AuthenticatorFunc(func(_ any) (bool, any, error) { count++ return true, valTheUser, nil }) failA := runtime.AuthenticatorFunc(func(_ any) (bool, any, error) { count++ return true, nil, errors.New("unauthenticated") }) ra3 := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuth2: successA, testAuth3: failA, testAuth4: successA, }, Schemes: []string{testAuth2, testAuth3, testAuth4}, Scopes: map[string][]string{testAuth2: nil}, } ras = RouteAuthenticators([]RouteAuthenticator{ra1, ra3}) require.FalseT(t, ras.AllowsAnonymous()) req, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route = &MatchedRoute{} ok, prin, err = ras.Authenticate(req, route) require.Error(t, err) require.TrueT(t, ok) require.Nil(t, prin) require.Equal(t, ra3, *route.Authenticator) require.EqualT(t, 2, count) ra4 := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuth2: successA, testAuth3: successA, testAuth4: failA, }, Schemes: []string{testAuth2, testAuth3, testAuth4}, Scopes: map[string][]string{testAuth2: nil}, } ras = RouteAuthenticators([]RouteAuthenticator{ra1, ra4}) require.FalseT(t, ras.AllowsAnonymous()) req, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route = &MatchedRoute{} ok, prin, err = ras.Authenticate(req, route) require.Error(t, err) require.TrueT(t, ok) require.Nil(t, prin) require.Equal(t, ra4, *route.Authenticator) require.EqualT(t, 5, count) } // TestAuthenticateTypedNilPrincipal is a regression test for // https://github.com/go-openapi/runtime/issues/147. // // When an authenticator is written with a concrete principal type (e.g. *myPrincipal), // returning (true, nil, nil) packs a typed-nil into the any return. A naive // `usr == nil` check would miss it and treat the typed-nil as a successful auth. // The Authenticate path must detect that case so the unauthenticated branch // (a 401 in Context.Authorize) is taken. func TestAuthenticateTypedNilPrincipal(t *testing.T) { type myPrincipal struct{} typedNilAuth := runtime.AuthenticatorFunc(func(_ any) (bool, any, error) { var p *myPrincipal // typed nil return true, p, nil }) ra := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuthenticator: typedNilAuth, }, Schemes: []string{testAuthenticator}, Scopes: map[string][]string{testAuthenticator: nil}, } ras := RouteAuthenticators([]RouteAuthenticator{ra}) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route := &MatchedRoute{} ok, prin, err := ras.Authenticate(req, route) require.NoError(t, err) // applies=true was returned but principal is (typed) nil: treated as // not-authenticated, so the aggregator must report no successful auth. require.FalseT(t, ok) require.Nil(t, prin) } func TestAuthenticateOptional(t *testing.T) { ra1 := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuthenticator: noApplyAuth, }, Schemes: []string{testAuthenticator}, Scopes: map[string][]string{testAuthenticator: nil}, } ra2 := RouteAuthenticator{ allowAnonymous: true, Schemes: []string{""}, Scopes: map[string][]string{"": {}}, } ra3 := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuth2: noApplyAuth, }, Schemes: []string{testAuth2}, Scopes: map[string][]string{testAuth2: nil}, } ras := RouteAuthenticators([]RouteAuthenticator{ra1, ra2, ra3}) require.TrueT(t, ras.AllowsAnonymous()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route := &MatchedRoute{} ok, prin, err := ras.Authenticate(req, route) require.NoError(t, err) require.TrueT(t, ok) require.Nil(t, prin) require.Equal(t, ra2, *route.Authenticator) ra4 := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuthenticator: noApplyAuth, }, Schemes: []string{testAuthenticator}, Scopes: map[string][]string{testAuthenticator: nil}, } ra5 := RouteAuthenticator{ allowAnonymous: true, } ra6 := RouteAuthenticator{ Authenticator: map[string]runtime.Authenticator{ testAuth2: failAuth, }, Schemes: []string{testAuth2}, Scopes: map[string][]string{testAuth2: nil}, } ras = RouteAuthenticators([]RouteAuthenticator{ra4, ra5, ra6}) require.TrueT(t, ras.AllowsAnonymous()) req, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) route = &MatchedRoute{} ok, prin, err = ras.Authenticate(req, route) require.Error(t, err) require.TrueT(t, ok) require.Nil(t, prin) require.Equal(t, ra6, *route.Authenticator) } go-openapi-runtime-decad8f/middleware/route_param_test.go000066400000000000000000000010311520232310000240710ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "testing" "github.com/go-openapi/testify/v2/assert" ) func TestRouteParams(t *testing.T) { coll1 := RouteParams([]RouteParam{ {"blah", "foo"}, {"abc", "bar"}, {"ccc", "efg"}, }) v := coll1.Get("blah") assert.EqualT(t, "foo", v) v2 := coll1.Get("abc") assert.EqualT(t, "bar", v2) v3 := coll1.Get("ccc") assert.EqualT(t, "efg", v3) v4 := coll1.Get("ydkdk") assert.Empty(t, v4) } go-openapi-runtime-decad8f/middleware/router.go000066400000000000000000000401331520232310000220420ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "fmt" "net/http" "net/url" fpath "path" "regexp" "strings" "github.com/go-openapi/analysis" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/logger" "github.com/go-openapi/runtime/middleware/denco" "github.com/go-openapi/runtime/security" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag/stringutils" "github.com/go-openapi/swag/typeutils" ) // RouteParam is a object to capture route params in a framework agnostic way. // implementations of the muxer should use these route params to communicate with the // swagger framework. type RouteParam struct { Name string Value string } // RouteParams the collection of route params. type RouteParams []RouteParam // Get gets the value for the route param for the specified key. func (r RouteParams) Get(name string) string { vv, _, _ := r.GetOK(name) if len(vv) > 0 { return vv[len(vv)-1] } return "" } // GetOK gets the value but also returns booleans to indicate if a key or value // is present. This aids in validation and satisfies an interface in use there. // // The returned values are: data, has key, has value. func (r RouteParams) GetOK(name string) ([]string, bool, bool) { for _, p := range r { if p.Name == name { return []string{p.Value}, true, p.Value != "" } } return nil, false, false } // NewRouter creates a new context-aware router [middleware]. func NewRouter(ctx *Context, next http.Handler) http.Handler { if ctx.router == nil { ctx.router = DefaultRouter(ctx.spec, ctx.api, WithDefaultRouterLoggerFunc(ctx.debugLogf)) } return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if _, rCtx, ok := ctx.RouteInfo(r); ok { next.ServeHTTP(rw, rCtx) return } // Always use the default producer Content-Type for Method not // allowed and Not found responses produces := []string{ctx.api.DefaultProduces()} // Not found, check if it exists in the other methods first if others := ctx.AllowedMethods(r); len(others) > 0 { ctx.Respond(rw, r, produces, nil, errors.MethodNotAllowed(r.Method, others)) return } ctx.Respond(rw, r, produces, nil, errors.NotFound("path %s was not found", r.URL.EscapedPath())) }) } // RoutableAPI represents an interface for things that can serve // as a provider of implementations for the swagger router. type RoutableAPI interface { HandlerFor(string, string) (http.Handler, bool) ServeErrorFor(string) func(http.ResponseWriter, *http.Request, error) ConsumersFor([]string) map[string]runtime.Consumer ProducersFor([]string) map[string]runtime.Producer AuthenticatorsFor(map[string]spec.SecurityScheme) map[string]runtime.Authenticator Authorizer() runtime.Authorizer Formats() strfmt.Registry DefaultProduces() string DefaultConsumes() string } // Router represents a swagger-aware router. type Router interface { Lookup(method, path string) (*MatchedRoute, bool) OtherMethods(method, path string) []string } type defaultRouteBuilder struct { spec *loads.Document analyzer *analysis.Spec api RoutableAPI records map[string][]denco.Record debugLogf func(string, ...any) // a logging function to debug context and all components using it } type defaultRouter struct { spec *loads.Document routers map[string]*denco.Router debugLogf func(string, ...any) // a logging function to debug context and all components using it } func newDefaultRouteBuilder(spec *loads.Document, api RoutableAPI, opts ...DefaultRouterOpt) *defaultRouteBuilder { var o defaultRouterOpts for _, apply := range opts { apply(&o) } if o.debugLogf == nil { o.debugLogf = debugLogfFunc(nil) // defaults to standard logger } return &defaultRouteBuilder{ spec: spec, analyzer: analysis.New(spec.Spec()), api: api, records: make(map[string][]denco.Record), debugLogf: o.debugLogf, } } // DefaultRouterOpt allows to inject optional behavior to the default router. type DefaultRouterOpt func(*defaultRouterOpts) type defaultRouterOpts struct { debugLogf func(string, ...any) } // WithDefaultRouterLogger sets the debug logger for the default router. // // This is enabled only in DEBUG mode. func WithDefaultRouterLogger(lg logger.Logger) DefaultRouterOpt { return func(o *defaultRouterOpts) { o.debugLogf = debugLogfFunc(lg) } } // WithDefaultRouterLoggerFunc sets a logging debug method for the default router. func WithDefaultRouterLoggerFunc(fn func(string, ...any)) DefaultRouterOpt { return func(o *defaultRouterOpts) { o.debugLogf = fn } } // DefaultRouter creates a default implementation of the router. func DefaultRouter(spec *loads.Document, api RoutableAPI, opts ...DefaultRouterOpt) Router { builder := newDefaultRouteBuilder(spec, api, opts...) if spec != nil { for method, paths := range builder.analyzer.Operations() { for path, operation := range paths { fp := fpath.Join(spec.BasePath(), path) builder.debugLogf("adding route %s %s %q", method, fp, operation.ID) builder.AddRoute(method, fp, operation) } } } return builder.Build() } // RouteAuthenticator is an authenticator that can compose several authenticators together. // It also knows when it contains an authenticator that allows for anonymous pass through. // Contains a group of 1 or more authenticators that have a logical AND relationship. type RouteAuthenticator struct { Authenticator map[string]runtime.Authenticator Schemes []string Scopes map[string][]string allScopes []string commonScopes []string allowAnonymous bool } func (ra *RouteAuthenticator) AllowsAnonymous() bool { return ra.allowAnonymous } // AllScopes returns a list of unique scopes that is the combination // of all the scopes in the requirements. func (ra *RouteAuthenticator) AllScopes() []string { return ra.allScopes } // CommonScopes returns a list of unique scopes that are common in all the // scopes in the requirements. func (ra *RouteAuthenticator) CommonScopes() []string { return ra.commonScopes } // Authenticate Authenticator interface implementation. func (ra *RouteAuthenticator) Authenticate(req *http.Request, route *MatchedRoute) (bool, any, error) { if ra.allowAnonymous { route.Authenticator = ra return true, nil, nil } // iterate in proper order var lastResult any for _, scheme := range ra.Schemes { if authenticator, ok := ra.Authenticator[scheme]; ok { applies, princ, err := authenticator.Authenticate(&security.ScopedAuthRequest{ Request: req, RequiredScopes: ra.Scopes[scheme], }) if !applies { return false, nil, nil } if err != nil { route.Authenticator = ra return true, nil, err } lastResult = princ } } route.Authenticator = ra return true, lastResult, nil } func stringSliceUnion(slices ...[]string) []string { unique := make(map[string]struct{}) var result []string for _, slice := range slices { for _, entry := range slice { if _, ok := unique[entry]; ok { continue } unique[entry] = struct{}{} result = append(result, entry) } } return result } func stringSliceIntersection(slices ...[]string) []string { unique := make(map[string]int) var intersection []string total := len(slices) var emptyCnt int for _, slice := range slices { if len(slice) == 0 { emptyCnt++ continue } for _, entry := range slice { unique[entry]++ if unique[entry] == total-emptyCnt { // this entry appeared in all the non-empty slices intersection = append(intersection, entry) } } } return intersection } // RouteAuthenticators represents a group of authenticators that represent a logical OR. type RouteAuthenticators []RouteAuthenticator // AllowsAnonymous returns true when there is an authenticator that means optional auth. func (ras RouteAuthenticators) AllowsAnonymous() bool { for _, ra := range ras { if ra.AllowsAnonymous() { return true } } return false } // Authenticate method implementation so this collection can be used as authenticator. func (ras RouteAuthenticators) Authenticate(req *http.Request, route *MatchedRoute) (bool, any, error) { var lastError error var allowsAnon bool var anonAuth RouteAuthenticator for _, ra := range ras { if ra.AllowsAnonymous() { anonAuth = ra allowsAnon = true continue } applies, usr, err := ra.Authenticate(req, route) if !applies || err != nil || typeutils.IsZero(usr) { if err != nil { lastError = err } continue } return applies, usr, nil } if allowsAnon && lastError == nil { route.Authenticator = &anonAuth return true, nil, lastError } return lastError != nil, nil, lastError } type routeEntry struct { PathPattern string BasePath string Operation *spec.Operation Consumes []string Consumers map[string]runtime.Consumer Produces []string Producers map[string]runtime.Producer Parameters map[string]spec.Parameter Handler http.Handler Formats strfmt.Registry Binder *UntypedRequestBinder Authenticators RouteAuthenticators Authorizer runtime.Authorizer } // MatchedRoute represents the route that was matched in this request. type MatchedRoute struct { routeEntry Params RouteParams Consumer runtime.Consumer Producer runtime.Producer Authenticator *RouteAuthenticator } // HasAuth returns true when the route has a security requirement defined. func (m *MatchedRoute) HasAuth() bool { return len(m.Authenticators) > 0 } // NeedsAuth returns true when the request still // needs to perform authentication. func (m *MatchedRoute) NeedsAuth() bool { return m.HasAuth() && m.Authenticator == nil } func (d *defaultRouter) Lookup(method, path string) (*MatchedRoute, bool) { mth := strings.ToUpper(method) d.debugLogf("looking up route for %s %s", method, path) if len(d.routers) == 0 { if Debug { d.debugLogf("there are no known routers") } panic("internal error: no router is configured") } if Debug { for meth := range d.routers { d.debugLogf("got a router for %s", meth) } } router, ok := d.routers[mth] if !ok { d.debugLogf("couldn't find a route by method for %s %s", method, path) return nil, false } m, rp, ok := router.Lookup(fpath.Clean(escapeLiteralColons(path))) if !ok || m == nil { d.debugLogf("couldn't find a route by path for %s %s", method, path) return nil, false } entry, ok := m.(*routeEntry) if !ok { return nil, false } d.debugLogf("found a route for %s %s with %d parameters", method, path, len(entry.Parameters)) var params RouteParams for _, p := range rp { v, err := url.PathUnescape(p.Value) if err != nil { d.debugLogf("failed to escape %q: %v", p.Value, err) v = p.Value } // a workaround to handle fragment/composing parameters until they are supported in denco router // check if this parameter is a fragment within a path segment const enclosureSize = 2 if xpos := strings.Index(entry.PathPattern, fmt.Sprintf("{%s}", p.Name)) + len(p.Name) + enclosureSize; xpos < len(entry.PathPattern) && entry.PathPattern[xpos] != '/' { // extract fragment parameters ep := strings.Split(entry.PathPattern[xpos:], "/")[0] pnames, pvalues := decodeCompositParams(p.Name, v, ep, nil, nil) for i, pname := range pnames { params = append(params, RouteParam{Name: pname, Value: pvalues[i]}) } } else { // use the parameter directly params = append(params, RouteParam{Name: p.Name, Value: v}) } } return &MatchedRoute{routeEntry: *entry, Params: params}, true } func (d *defaultRouter) OtherMethods(method, path string) []string { mn := strings.ToUpper(method) var methods []string for k, v := range d.routers { if k != mn { if _, _, ok := v.Lookup(fpath.Clean(escapeLiteralColons(path))); ok { methods = append(methods, k) continue } } } return methods } func (d *defaultRouter) SetLogger(lg logger.Logger) { d.debugLogf = debugLogfFunc(lg) } // convert swagger parameters per path segment into a denco parameter as multiple parameters per segment are not supported in denco. var pathConverter = regexp.MustCompile(`{(.+?)}([^/]*)`) // escapeLiteralColons replaces literal ':' characters with their URL-encoded // equivalent "%3A". This prevents the denco router from misinterpreting ':' // in URL path segments as parameter delimiters. The ':' character is valid in // URL paths per RFC 3986 section 3.3. func escapeLiteralColons(path string) string { return strings.ReplaceAll(path, ":", "%3A") } func decodeCompositParams(name string, value string, pattern string, names []string, values []string) ([]string, []string) { pleft := strings.Index(pattern, "{") names = append(names, name) if pleft < 0 { if strings.HasSuffix(value, pattern) { values = append(values, value[:len(value)-len(pattern)]) } else { values = append(values, "") } return names, values } toskip := pattern[:pleft] pright := strings.Index(pattern, "}") vright := strings.Index(value, toskip) if vright >= 0 { values = append(values, value[:vright]) } else { values = append(values, "") value = "" } return decodeCompositParams(pattern[pleft+1:pright], value[vright+len(toskip):], pattern[pright+1:], names, values) } func (d *defaultRouteBuilder) AddRoute(method, path string, operation *spec.Operation) { mn := strings.ToUpper(method) bp := fpath.Clean(d.spec.BasePath()) if len(bp) > 0 && bp[len(bp)-1] == '/' { bp = bp[:len(bp)-1] } d.debugLogf("operation: %#v", *operation) if handler, ok := d.api.HandlerFor(method, strings.TrimPrefix(path, bp)); ok { consumes := d.analyzer.ConsumesFor(operation) produces := d.analyzer.ProducesFor(operation) parameters := d.analyzer.ParamsFor(method, strings.TrimPrefix(path, bp)) // add API defaults if not part of the spec if defConsumes := d.api.DefaultConsumes(); defConsumes != "" && !stringutils.ContainsStringsCI(consumes, defConsumes) { consumes = append(consumes, defConsumes) } if defProduces := d.api.DefaultProduces(); defProduces != "" && !stringutils.ContainsStringsCI(produces, defProduces) { produces = append(produces, defProduces) } requestBinder := NewUntypedRequestBinder(parameters, d.spec.Spec(), d.api.Formats()) requestBinder.setDebugLogf(d.debugLogf) record := denco.NewRecord(pathConverter.ReplaceAllString(escapeLiteralColons(path), ":$1"), &routeEntry{ BasePath: bp, PathPattern: path, Operation: operation, Handler: handler, Consumes: consumes, Produces: produces, Consumers: d.api.ConsumersFor(normalizeOffers(consumes)), Producers: d.api.ProducersFor(normalizeOffers(produces)), Parameters: parameters, Formats: d.api.Formats(), Binder: requestBinder, Authenticators: d.buildAuthenticators(operation), Authorizer: d.api.Authorizer(), }) d.records[mn] = append(d.records[mn], record) } } func (d *defaultRouteBuilder) Build() *defaultRouter { routers := make(map[string]*denco.Router) for method, records := range d.records { router := denco.New() _ = router.Build(records) routers[method] = router } return &defaultRouter{ spec: d.spec, routers: routers, debugLogf: d.debugLogf, } } func (d *defaultRouteBuilder) buildAuthenticators(operation *spec.Operation) RouteAuthenticators { requirements := d.analyzer.SecurityRequirementsFor(operation) auths := make([]RouteAuthenticator, 0, len(requirements)) for _, reqs := range requirements { schemes := make([]string, 0, len(reqs)) scopes := make(map[string][]string, len(reqs)) scopeSlices := make([][]string, 0, len(reqs)) for _, req := range reqs { schemes = append(schemes, req.Name) scopes[req.Name] = req.Scopes scopeSlices = append(scopeSlices, req.Scopes) } definitions := d.analyzer.SecurityDefinitionsForRequirements(reqs) authenticators := d.api.AuthenticatorsFor(definitions) auths = append(auths, RouteAuthenticator{ Authenticator: authenticators, Schemes: schemes, Scopes: scopes, allScopes: stringSliceUnion(scopeSlices...), commonScopes: stringSliceIntersection(scopeSlices...), allowAnonymous: len(reqs) == 1 && reqs[0].Name == "", }) } return auths } go-openapi-runtime-decad8f/middleware/router_test.go000066400000000000000000000264331520232310000231100ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( stdcontext "context" "net/http" "net/http/httptest" "sort" "strings" "testing" "github.com/go-openapi/analysis" "github.com/go-openapi/loads" "github.com/go-openapi/runtime/internal/testing/petstore" "github.com/go-openapi/runtime/middleware/denco" "github.com/go-openapi/runtime/middleware/untyped" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func terminator(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusOK) } func TestRouterMiddleware(t *testing.T) { spec, api := petstore.NewAPI(t) context := NewContext(spec, api, nil) mw := NewRouter(context, http.HandlerFunc(terminator)) recorder := httptest.NewRecorder() request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) recorder = httptest.NewRecorder() request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodDelete, "/api/pets", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, jsonMime, recorder.Header().Get("Content-Type")) assert.EqualT(t, http.StatusMethodNotAllowed, recorder.Code) methods := strings.Split(recorder.Header().Get("Allow"), ",") sort.Strings(methods) assert.EqualT(t, "GET,POST", strings.Join(methods, ",")) recorder = httptest.NewRecorder() request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/nopets", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, jsonMime, recorder.Header().Get("Content-Type")) assert.EqualT(t, http.StatusNotFound, recorder.Code) recorder = httptest.NewRecorder() request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/pets", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusNotFound, recorder.Code) spec, api = petstore.NewRootAPI(t) context = NewContext(spec, api, nil) mw = NewRouter(context, http.HandlerFunc(terminator)) recorder = httptest.NewRecorder() request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/pets", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) recorder = httptest.NewRecorder() request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodDelete, "/pets", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusMethodNotAllowed, recorder.Code) methods = strings.Split(recorder.Header().Get("Allow"), ",") sort.Strings(methods) assert.EqualT(t, "GET,POST", strings.Join(methods, ",")) recorder = httptest.NewRecorder() request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/nopets", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusNotFound, recorder.Code) } func TestRouterBuilder(t *testing.T) { spec, api := petstore.NewAPI(t) analyzed := analysis.New(spec.Spec()) assert.Len(t, analyzed.RequiredConsumes(), 3) assert.Len(t, analyzed.RequiredProduces(), 5) assert.Len(t, analyzed.OperationIDs(), 4) // context := NewContext(spec, api) builder := petAPIRouterBuilder(spec, api, analyzed) getRecords := builder.records[http.MethodGet] postRecords := builder.records[http.MethodPost] deleteRecords := builder.records[http.MethodDelete] assert.Len(t, getRecords, 2) assert.Len(t, postRecords, 1) assert.Len(t, deleteRecords, 1) assert.Empty(t, builder.records[http.MethodPatch]) assert.Empty(t, builder.records[http.MethodOptions]) assert.Empty(t, builder.records[http.MethodHead]) assert.Empty(t, builder.records[http.MethodPut]) rec := postRecords[0] assert.EqualT(t, "/pets", rec.Key) val, ok := rec.Value.(*routeEntry) require.TrueT(t, ok) assert.Len(t, val.Consumers, 2) assert.Len(t, val.Producers, 2) assert.Len(t, val.Consumes, 2) assert.Len(t, val.Produces, 2) assert.MapContainsT(t, val.Consumers, jsonMime) assert.MapContainsT(t, val.Producers, "application/x-yaml") assert.SliceContainsT(t, val.Consumes, jsonMime) assert.SliceContainsT(t, val.Produces, "application/x-yaml") assert.Len(t, val.Parameters, 1) recG := getRecords[0] assert.EqualT(t, "/pets", recG.Key) valG, ok := recG.Value.(*routeEntry) require.TrueT(t, ok) assert.Len(t, valG.Consumers, 2) assert.Len(t, valG.Producers, 4) assert.Len(t, valG.Consumes, 2) assert.Len(t, valG.Produces, 4) assert.Len(t, valG.Parameters, 2) } func TestRouterCanonicalBasePath(t *testing.T) { spec, api := petstore.NewAPI(t) spec.Spec().BasePath = "/api///" context := NewContext(spec, api, nil) mw := NewRouter(context, http.HandlerFunc(terminator)) recorder := httptest.NewRecorder() request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) } func TestRouter_EscapedPath(t *testing.T) { spec, api := petstore.NewAPI(t) spec.Spec().BasePath = "/api/" context := NewContext(spec, api, nil) mw := NewRouter(context, http.HandlerFunc(terminator)) recorder := httptest.NewRecorder() request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets/123", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) recorder = httptest.NewRecorder() request, err = http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets/abc%2Fdef", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) ri, _, _ := context.RouteInfo(request) require.NotNil(t, ri) require.NotNil(t, ri.Params) assert.EqualT(t, "abc/def", ri.Params.Get(paramKeyID)) } func TestRouterStruct(t *testing.T) { spec, api := petstore.NewAPI(t) router := DefaultRouter(spec, newRoutableUntypedAPI(spec, api, new(Context))) methods := router.OtherMethods("post", "/api/pets/{id}") assert.Len(t, methods, 2) entry, ok := router.Lookup("delete", "/api/pets/{id}") assert.TrueT(t, ok) require.NotNil(t, entry) assert.Len(t, entry.Params, 1) assert.EqualT(t, paramKeyID, entry.Params[0].Name) _, ok = router.Lookup("delete", "/pets") assert.FalseT(t, ok) _, ok = router.Lookup("post", "/no-pets") assert.FalseT(t, ok) } func petAPIRouterBuilder(spec *loads.Document, api *untyped.API, analyzed *analysis.Spec) *defaultRouteBuilder { builder := newDefaultRouteBuilder(spec, newRoutableUntypedAPI(spec, api, new(Context))) builder.AddRoute(http.MethodGet, "/pets", analyzed.AllPaths()["/pets"].Get) builder.AddRoute(http.MethodPost, "/pets", analyzed.AllPaths()["/pets"].Post) builder.AddRoute(http.MethodDelete, "/pets/{id}", analyzed.AllPaths()["/pets/{id}"].Delete) builder.AddRoute(http.MethodGet, "/pets/{id}", analyzed.AllPaths()["/pets/{id}"].Get) return builder } func TestPathConverter(t *testing.T) { cases := []struct { swagger string denco string }{ {"/", "/"}, {pathSomething, pathSomething}, {"/{id}", "/:id"}, {"/{id}/something/{anotherId}", "/:id/something/:anotherId"}, {"/{petid}", "/:petid"}, {"/{pet_id}", "/:pet_id"}, {"/{petId}", "/:petId"}, {"/{pet-id}", "/:pet-id"}, // compost parameters tests {"/p_{pet_id}", "/p_:pet_id"}, {"/p_{petId}.{petSubId}", "/p_:petId"}, } for _, tc := range cases { actual := pathConverter.ReplaceAllString(tc.swagger, ":$1") assert.EqualT(t, tc.denco, actual, "expected swagger path %s to match %s but got %s", tc.swagger, tc.denco, actual) } } func TestEscapeLiteralColons(t *testing.T) { cases := []struct { input string expected string }{ {"/", "/"}, {pathSomething, pathSomething}, {"/allow/{serverName}/tokenlist:add", "/allow/{serverName}/tokenlist%3Aadd"}, {"/path:with:colons", "/path%3Awith%3Acolons"}, {"/{id}:{name}", "/{id}%3A{name}"}, {"/action:do/{id}", "/action%3Ado/{id}"}, {"/normal/path", "/normal/path"}, } for _, tc := range cases { actual := escapeLiteralColons(tc.input) assert.EqualT(t, tc.expected, actual, "expected escapeLiteralColons(%s) to be %s but got %s", tc.input, tc.expected, actual) } } func TestPathConverterWithLiteralColons(t *testing.T) { // Verify the full pipeline: escapeLiteralColons then pathConverter cases := []struct { swagger string denco string }{ // The main use case from issue #352 {"/allow/{serverName}/tokenlist:add", "/allow/:serverName/tokenlist%3Aadd"}, // Literal colons in static segments {"/action:do/{id}", "/action%3Ado/:id"}, {"/path:with:colons", "/path%3Awith%3Acolons"}, // Multiple colons in different segments {"/api:v1/items/{id}", "/api%3Av1/items/:id"}, } for _, tc := range cases { actual := pathConverter.ReplaceAllString(escapeLiteralColons(tc.swagger), ":$1") assert.EqualT(t, tc.denco, actual, "expected swagger path %s to produce denco path %s but got %s", tc.swagger, tc.denco, actual) } } func TestDencoRouterWithLiteralColons(t *testing.T) { // Test that the denco router correctly handles paths with literal colons // when the colons are URL-encoded as %3A. t.Run("static path with encoded colons", func(t *testing.T) { router := denco.New() err := router.Build([]denco.Record{ denco.NewRecord("/path%3Awith%3Acolons", "static-colon-route"), }) require.NoError(t, err) data, _, ok := router.Lookup("/path%3Awith%3Acolons") assert.TrueT(t, ok) assert.EqualT(t, "static-colon-route", data) // Should not match the unescaped version _, _, ok = router.Lookup("/path:with:colons") assert.FalseT(t, ok) }) t.Run("parametric path with encoded colons in suffix", func(t *testing.T) { router := denco.New() err := router.Build([]denco.Record{ denco.NewRecord("/allow/:serverName/tokenlist%3Aadd", "param-colon-route"), }) require.NoError(t, err) data, params, ok := router.Lookup("/allow/myserver/tokenlist%3Aadd") assert.TrueT(t, ok) assert.EqualT(t, "param-colon-route", data) require.Len(t, params, 1) assert.EqualT(t, "myserver", params[0].Value) }) t.Run("parametric path with encoded colons between params", func(t *testing.T) { router := denco.New() err := router.Build([]denco.Record{ denco.NewRecord("/:id/items%3Acheck/:name", "between-params-route"), }) require.NoError(t, err) data, params, ok := router.Lookup("/foo/items%3Acheck/bar") assert.TrueT(t, ok) assert.EqualT(t, "between-params-route", data) require.Len(t, params, 2) assert.EqualT(t, "foo", params[0].Value) assert.EqualT(t, "bar", params[1].Value) }) } func TestExtractCompositParameters(t *testing.T) { // name is the composite parameter's name, value is the value of this compost parameter, pattern is the pattern to be matched cases := []struct { name string value string pattern string names []string values []string }{ {name: valFragment, value: "gie", pattern: "e", names: []string{valFragment}, values: []string{"gi"}}, {name: valFragment, value: "t.simpson", pattern: ".{subfragment}", names: []string{valFragment, "subfragment"}, values: []string{"t", "simpson"}}, } for _, tc := range cases { names, values := decodeCompositParams(tc.name, tc.value, tc.pattern, nil, nil) assert.Equal(t, tc.names, names) assert.Equal(t, tc.values, values) } } go-openapi-runtime-decad8f/middleware/seam.go000066400000000000000000000315451520232310000214560ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "net/http" "path" "strings" "github.com/go-openapi/runtime/server-middleware/docui" "github.com/go-openapi/runtime/server-middleware/negotiate" ) /////////////////////////////////////////////////////////: // Seam to the negotiate options introduced in v0.29.5 /////////////////////////////////////////////////////////: // NegotiateOption configures [NegotiateContentType] behaviour. // // Deprecated: moved to the [negotiate] package. Use [negotiate.Option] instead. type NegotiateOption = negotiate.Option // NegotiateContentType returns the best offered content type for the // request's Accept header. // // Deprecated: moved to the [negotiate] package. Use [negotiate.ContentType] instead. func NegotiateContentType(r *http.Request, offers []string, defaultOffer string, opts ...NegotiateOption) string { return negotiate.ContentType(r, offers, defaultOffer, opts...) } // NegotiateContentEncoding returns the best offered content encoding for // the request's Accept-Encoding header. // // Deprecated: moved to the [negotiate] package. Use [negotiate.ContentEncoding] instead. func NegotiateContentEncoding(r *http.Request, offers []string) string { return negotiate.ContentEncoding(r, offers) } // WithIgnoreParameters returns a [NegotiateOption] that strips MIME-type // parameters from both Accept entries and offers before matching, // restoring the pre-v0.30 behaviour. // // Deprecated: moved to the [negotiate] package. Use [negotiate.WithIgnoreParameters] instead. func WithIgnoreParameters(ignore bool) NegotiateOption { return negotiate.WithIgnoreParameters(ignore) } /////////////////////////////////////////////////////////: // Seam to the UI options /////////////////////////////////////////////////////////: // RapiDoc creates a [http.Handler] to serve a documentation site for a swagger spec. // // This allows for altering the spec before starting the [http] listener. // // Deprecated: moved to the [docui] package. Use [docui.RapiDoc] instead. func RapiDoc(opts RapiDocOpts, next http.Handler) http.Handler { return docui.RapiDoc(next, opts.toFuncOptions()...) } // Redoc creates a [http.Handler] to serve a documentation site for a swagger spec. // // This allows for altering the spec before starting the [http] listener. // // Deprecated: moved to the [docui] package. Use [docui.Redoc] instead. func Redoc(opts RedocOpts, next http.Handler) http.Handler { return docui.Redoc(next, opts.toFuncOptions()...) } // SwaggerUI creates a [http.Handler] to serve a documentation site for a swagger spec. // // This allows for altering the spec before starting the [http] listener. // // Deprecated: moved to the [docui] package. Use [docui.SwaggerUI] instead. func SwaggerUI(opts SwaggerUIOpts, next http.Handler) http.Handler { return docui.SwaggerUI(next, opts.toFuncOptions()...) } // SwaggerUIOAuth2Callback creates a middleware that serves the OAuth2 callback page used by Swagger UI. // // Deprecated: moved to the [docui] package. Use [docui.SwaggerUIOAuth2Callback] instead. func SwaggerUIOAuth2Callback(opts SwaggerUIOpts, next http.Handler) http.Handler { return docui.SwaggerUIOAuth2Callback(next, opts.toFuncOptions()...) } /////////////////////////////////////////////////////////: // Seam to the spec middleware options /////////////////////////////////////////////////////////: // SpecOption can be applied to the [Spec] serving [middleware]. // // Deprecated: moved to the [docui] package. Use [docui.SpecOption] instead. type SpecOption func(*specOptions) type specOptions struct { BasePath string Path string Document string } func (o specOptions) fullPath() string { return path.Join(o.BasePath, o.Path, o.Document) } func specOptionsWithDefaults(basePath string, opts []SpecOption) specOptions { o := specOptions{ BasePath: "/", Path: "", Document: "swagger.json", } for _, apply := range opts { apply(&o) } if basePath != "" { o.BasePath = basePath } return o } // Spec creates a [middleware] to serve a swagger spec as a JSON document. // // This allows for altering the spec before starting the [http] listener. // // The basePath argument indicates the path of the spec document (defaults to "/"). // Additional [SpecOption] can be used to change the name of the document (defaults to "swagger.json"). // // Deprecated: moved to the [docui] package as [docui.ServeSpec]. func Spec(basePath string, spec []byte, next http.Handler, opts ...SpecOption) http.Handler { o := specOptionsWithDefaults(basePath, opts) return docui.ServeSpec(spec, next, docui.WithSpecPath(o.fullPath())) } // WithSpecPath sets the path to be joined to the base path of the // spec-serving middleware (see [docui.ServeSpec]). // // This is empty by default. func WithSpecPath(pth string) SpecOption { return func(o *specOptions) { o.Path = pth } } // WithSpecDocument sets the name of the JSON document served as a spec. // // By default, this is "swagger.json". func WithSpecDocument(doc string) SpecOption { return func(o *specOptions) { if doc == "" { return } o.Document = doc } } // UIOptions defines common options for UI serving middlewares. // // Deprecated: use instead the function options provided by [docui]. type UIOptions struct { // BasePath for the UI, defaults to: / BasePath string // Path combines with BasePath to construct the path to the UI, defaults to: "docs". Path string // SpecURL is the URL of the spec document. // // Defaults to: /swagger.json SpecURL string // Title for the documentation site, default to: API documentation Title string // Template specifies a custom template to serve the UI Template string } // toFuncOptions bridges the deprecated options struct with the newer function options in [docui]. func (o UIOptions) toFuncOptions() []docui.Option { const structMembers = 5 opts := make([]docui.Option, 0, structMembers) if o.BasePath != "" { opts = append(opts, docui.WithUIBasePath(o.BasePath)) } if o.Path != "" { opts = append(opts, docui.WithUIPath(o.Path)) } if o.SpecURL != "" { opts = append(opts, docui.WithSpecURL(o.SpecURL)) } if o.Title != "" { opts = append(opts, docui.WithUITitle(o.Title)) } if o.Template != "" { opts = append(opts, docui.WithUITemplate(o.Template)) } return opts } // RapiDocOpts configures the [RapiDoc] middlewares. // // Deprecated: use instead the function options provided by [docui]. type RapiDocOpts struct { // BasePath for the UI, defaults to: / BasePath string // Path combines with BasePath to construct the path to the UI, defaults to: "docs". Path string // SpecURL is the URL of the spec document. // // Defaults to: /swagger.json SpecURL string // Title for the documentation site, default to: API documentation Title string // Template specifies a custom template to serve the UI Template string // RapiDocURL points to the js asset that generates the rapidoc site. // // Defaults to https://unpkg.com/rapidoc/dist/rapidoc-min.js RapiDocURL string } func (o RapiDocOpts) toFuncOptions() []docui.Option { const structMembers = 6 opts := make([]docui.Option, 0, structMembers) if o.BasePath != "" { opts = append(opts, docui.WithUIBasePath(o.BasePath)) } if o.Path != "" { opts = append(opts, docui.WithUIPath(o.Path)) } if o.SpecURL != "" { opts = append(opts, docui.WithSpecURL(o.SpecURL)) } if o.Title != "" { opts = append(opts, docui.WithUITitle(o.Title)) } if o.Template != "" { opts = append(opts, docui.WithUITemplate(o.Template)) } if o.RapiDocURL != "" { opts = append(opts, docui.WithUIAssetsURL(o.RapiDocURL)) } return opts } // RedocOpts configures the [Redoc] middlewares. // // Deprecated: use instead the function options provided by [docui]. type RedocOpts struct { // BasePath for the UI, defaults to: / BasePath string // Path combines with BasePath to construct the path to the UI, defaults to: "docs". Path string // SpecURL is the URL of the spec document. // // Defaults to: /swagger.json SpecURL string // Title for the documentation site, default to: API documentation Title string // Template specifies a custom template to serve the UI Template string // RedocURL points to the js that generates the redoc site. // // Defaults to: https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js RedocURL string } func (o RedocOpts) toFuncOptions() []docui.Option { const structMembers = 6 opts := make([]docui.Option, 0, structMembers) if o.BasePath != "" { opts = append(opts, docui.WithUIBasePath(o.BasePath)) } if o.Path != "" { opts = append(opts, docui.WithUIPath(o.Path)) } if o.SpecURL != "" { opts = append(opts, docui.WithSpecURL(o.SpecURL)) } if o.Title != "" { opts = append(opts, docui.WithUITitle(o.Title)) } if o.Template != "" { opts = append(opts, docui.WithUITemplate(o.Template)) } if o.RedocURL != "" { opts = append(opts, docui.WithUIAssetsURL(o.RedocURL)) } return opts } // SwaggerUIOpts configures the [SwaggerUI] [middleware]. // // Deprecated: use instead the function options provided by [docui]. type SwaggerUIOpts struct { // BasePath for the API, defaults to: / BasePath string // Path combines with BasePath to construct the path to the UI, defaults to: "docs". Path string // SpecURL is the URL of the spec document. // // Defaults to: /swagger.json SpecURL string // Title for the documentation site, default to: API documentation Title string // Template specifies a custom template to serve the UI Template string // OAuthCallbackURL the url called after OAuth2 login // // NOTE: in the new [docui.SwaggerUIOptions] type, this field is named `OAuth2CallbackURL`, // which is more appropriate. OAuthCallbackURL string // The three components needed to embed swagger-ui // SwaggerURL points to the js that generates the SwaggerUI site. // // Defaults to: https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js SwaggerURL string SwaggerPresetURL string SwaggerStylesURL string Favicon32 string Favicon16 string } func (o SwaggerUIOpts) toFuncOptions() []docui.Option { const structMembers = 6 opts := make([]docui.Option, 0, structMembers) if o.BasePath != "" { opts = append(opts, docui.WithUIBasePath(o.BasePath)) } if o.Path != "" { opts = append(opts, docui.WithUIPath(o.Path)) } if o.SpecURL != "" { opts = append(opts, docui.WithSpecURL(o.SpecURL)) } if o.Title != "" { opts = append(opts, docui.WithUITitle(o.Title)) } if o.Template != "" { opts = append(opts, docui.WithUITemplate(o.Template)) } if o.SwaggerURL != "" { opts = append(opts, docui.WithUIAssetsURL(o.SwaggerURL)) } var empty SwaggerUIOpts if o != empty { swaggeruiOpts := docui.SwaggerUIOptions{ OAuth2CallbackURL: o.OAuthCallbackURL, SwaggerPresetURL: o.SwaggerPresetURL, SwaggerStylesURL: o.SwaggerStylesURL, Favicon32: o.Favicon32, Favicon16: o.Favicon16, } opts = append(opts, docui.WithSwaggerUIOptions(swaggeruiOpts)) } return opts } // UIOption can be applied to UI serving [middleware] to alter the default // behavior. // // Deprecated: use instead the function options provided by [docui]. type UIOption func(*UIOptions) // uiOptionsWithDefaults applies the given options on top of an empty // [UIOptions]. Per-flavor handlers ([SwaggerUI], [Redoc], [RapiDoc]) // fill in the remaining defaults via [UIOptions.EnsureDefaults] when // the option struct is used. func uiOptionsWithDefaults(opts []UIOption) UIOptions { var o UIOptions for _, apply := range opts { apply(&o) } return o } // WithUIBasePath sets the base path from where to serve the UI assets. // // Deprecated: use instead the function options provided by [docui]. func WithUIBasePath(base string) UIOption { return func(o *UIOptions) { if !strings.HasPrefix(base, "/") { base = "/" + base } o.BasePath = base } } // WithUIPath sets the path from where to serve the UI assets (i.e. /{basepath}/{path}. // // Deprecated: use instead the function options provided by [docui]. func WithUIPath(pth string) UIOption { return func(o *UIOptions) { o.Path = pth } } // WithUISpecURL sets the path from where to serve swagger spec document. // // This may be specified as a full URL or a path. // // By default, this is "/swagger.json". // // Deprecated: use instead the function options provided by [docui]. func WithUISpecURL(specURL string) UIOption { return func(o *UIOptions) { o.SpecURL = specURL } } // WithUITitle sets the title of the UI. // // Deprecated: use instead the function options provided by [docui]. func WithUITitle(title string) UIOption { return func(o *UIOptions) { o.Title = title } } // WithTemplate allows to set a custom template for the UI. // // UI [middleware] will panic if the template does not parse or execute properly. // // Deprecated: use instead the function options provided by [docui]. func WithTemplate(tpl string) UIOption { return func(o *UIOptions) { o.Template = tpl } } go-openapi-runtime-decad8f/middleware/seam_test.go000066400000000000000000000100331520232310000225020ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware_test // Smoke tests for the deprecated middleware aliases that forward to the // docui and negotiate packages. These verify that: // // - the type aliases still resolve so user code keeps compiling, // - the function-value aliases still produce the documented behaviour. // // The exhaustive coverage lives in the destination packages themselves. import ( "context" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" "github.com/go-openapi/runtime/middleware" ) const jsonMime = "application/json" func TestDeprecatedDocUIForwarders(t *testing.T) { t.Run("middleware.SwaggerUI still serves the docs page", func(t *testing.T) { h := middleware.SwaggerUI(middleware.SwaggerUIOpts{}, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) assert.EqualT(t, http.StatusOK, rec.Code) }) t.Run("middleware.Redoc still serves the docs page", func(t *testing.T) { h := middleware.Redoc(middleware.RedocOpts{}, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) assert.EqualT(t, http.StatusOK, rec.Code) }) t.Run("middleware.RapiDoc still serves the docs page", func(t *testing.T) { h := middleware.RapiDoc(middleware.RapiDocOpts{}, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) assert.EqualT(t, http.StatusOK, rec.Code) }) t.Run("middleware.SwaggerUIOAuth2Callback still serves the callback page", func(t *testing.T) { h := middleware.SwaggerUIOAuth2Callback(middleware.SwaggerUIOpts{}, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs/oauth2-callback", nil) require.NoError(t, err) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) assert.EqualT(t, http.StatusOK, rec.Code) }) t.Run("middleware.Spec still serves the spec document", func(t *testing.T) { body := []byte(`{"swagger":"2.0"}`) h := middleware.Spec("", body, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/swagger.json", nil) require.NoError(t, err) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) assert.EqualT(t, http.StatusOK, rec.Code) assert.EqualT(t, string(body), rec.Body.String()) }) t.Run("middleware.NegotiateContentType still selects the offered type", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) require.NoError(t, err) req.Header.Set("Accept", jsonMime) got := middleware.NegotiateContentType(req, []string{jsonMime}, "") assert.EqualT(t, jsonMime, got) }) t.Run("middleware.NegotiateContentEncoding still selects gzip", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) require.NoError(t, err) req.Header.Set("Accept-Encoding", "gzip") got := middleware.NegotiateContentEncoding(req, []string{"identity", "gzip"}) assert.EqualT(t, "gzip", got) }) t.Run("middleware.WithIgnoreParameters still strips params", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) require.NoError(t, err) req.Header.Set("Accept", "text/plain;charset=ascii") // Strict (default): no match because charset disagrees. strict := middleware.NegotiateContentType(req, []string{"text/plain;charset=utf-8"}, "fallback") assert.EqualT(t, "fallback", strict) // Loose (legacy mode): bare types agree, offer picked. loose := middleware.NegotiateContentType(req, []string{"text/plain;charset=utf-8"}, "fallback", middleware.WithIgnoreParameters(true), ) assert.EqualT(t, "text/plain;charset=utf-8", loose) }) } go-openapi-runtime-decad8f/middleware/security.go000066400000000000000000000011401520232310000223640ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import "net/http" func newSecureAPI(ctx *Context, next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { route, rCtx, _ := ctx.RouteInfo(r) if rCtx != nil { r = rCtx } if route != nil && !route.NeedsAuth() { next.ServeHTTP(rw, r) return } _, rCtx, err := ctx.Authorize(r, route) if err != nil { ctx.Respond(rw, r, route.Produces, route, err) return } r = rCtx next.ServeHTTP(rw, r) }) } go-openapi-runtime-decad8f/middleware/security_test.go000066400000000000000000000036751520232310000234420ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( stdcontext "context" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" "github.com/go-openapi/runtime/internal/testing/petstore" ) func TestSecurityMiddleware(t *testing.T) { spec, api := petstore.NewAPI(t) context := NewContext(spec, api, nil) context.router = DefaultRouter(spec, context.api) mw := newSecureAPI(context, http.HandlerFunc(terminator)) t.Run("without auth", func(t *testing.T) { recorder := httptest.NewRecorder() request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusUnauthorized, recorder.Code) }) t.Run("with wrong password", func(t *testing.T) { recorder := httptest.NewRecorder() request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) request.SetBasicAuth("admin", "wrong") mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusUnauthorized, recorder.Code) assert.NotEmpty(t, recorder.Header().Get("WWW-Authenticate")) }) t.Run("with correct password", func(t *testing.T) { recorder := httptest.NewRecorder() request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) request.SetBasicAuth("admin", "admin") mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) }) t.Run("with unauthenticated path", func(t *testing.T) { recorder := httptest.NewRecorder() request, err := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "//apipets/1", nil) require.NoError(t, err) mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) }) } go-openapi-runtime-decad8f/middleware/string_conversion_test.go000066400000000000000000000230261520232310000253360ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "errors" "reflect" "strings" "testing" "time" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag/stringutils" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) var evaluatesAsTrue = []string{"true", "1", "yes", "ok", "y", "on", "selected", "checked", "t", "enabled"} type unmarshallerSlice []string func (u *unmarshallerSlice) UnmarshalText(data []byte) error { if len(data) == 0 { return errors.New("an error") } *u = strings.Split(string(data), ",") return nil } type SomeOperationParams struct { Name string ID int64 Confirmed bool Age int Visits int32 Count int16 Seq int8 UID uint64 UAge uint UVisits uint32 UCount uint16 USeq uint8 Score float32 Rate float64 Timestamp strfmt.DateTime Birthdate strfmt.Date LastFailure *strfmt.DateTime Unsupported struct{} Tags []string Prefs []int32 Categories unmarshallerSlice } func FloatParamTest(t *testing.T, _, pName, _ string, val reflect.Value, defVal, expectedDef any, actual func() any) { fld := val.FieldByName(pName) binder := &untypedParamBinder{ parameter: spec.QueryParam(pName).Typed("number", "double").WithDefault(defVal), Name: pName, } err := binder.setFieldValue(fld, defVal, "5", true) require.NoError(t, err) assert.EqualValues(t, 5, actual()) err = binder.setFieldValue(fld, defVal, "", true) require.NoError(t, err) assert.EqualValues(t, expectedDef, actual()) err = binder.setFieldValue(fld, defVal, "yada", true) require.Error(t, err) } func IntParamTest(t *testing.T, pName string, val reflect.Value, defVal, expectedDef any, actual func() any) { fld := val.FieldByName(pName) binder := &untypedParamBinder{ parameter: spec.QueryParam(pName).Typed("integer", "int64").WithDefault(defVal), Name: pName, } err := binder.setFieldValue(fld, defVal, "5", true) require.NoError(t, err) assert.EqualValues(t, 5, actual()) err = binder.setFieldValue(fld, defVal, "", true) require.NoError(t, err) assert.EqualValues(t, expectedDef, actual()) err = binder.setFieldValue(fld, defVal, "yada", true) require.Error(t, err) } func TestParamBinding(t *testing.T) { actual := new(SomeOperationParams) val := reflect.ValueOf(actual).Elem() pName := keyName fld := val.FieldByName(pName) binder := &untypedParamBinder{ parameter: spec.QueryParam(pName).Typed(typeString, "").WithDefault("some-name"), Name: pName, } err := binder.setFieldValue(fld, "some-name", "the name value", true) require.NoError(t, err) assert.EqualT(t, "the name value", actual.Name) err = binder.setFieldValue(fld, "some-name", "", true) require.NoError(t, err) assert.EqualT(t, "some-name", actual.Name) IntParamTest(t, "ID", val, 1, 1, func() any { return actual.ID }) IntParamTest(t, "ID", val, nil, 0, func() any { return actual.ID }) IntParamTest(t, "Age", val, 1, 1, func() any { return actual.Age }) IntParamTest(t, "Age", val, nil, 0, func() any { return actual.Age }) IntParamTest(t, "Visits", val, 1, 1, func() any { return actual.Visits }) IntParamTest(t, "Visits", val, nil, 0, func() any { return actual.Visits }) IntParamTest(t, "Count", val, 1, 1, func() any { return actual.Count }) IntParamTest(t, "Count", val, nil, 0, func() any { return actual.Count }) IntParamTest(t, "Seq", val, 1, 1, func() any { return actual.Seq }) IntParamTest(t, "Seq", val, nil, 0, func() any { return actual.Seq }) IntParamTest(t, "UID", val, uint64(1), 1, func() any { return actual.UID }) IntParamTest(t, "UID", val, uint64(0), 0, func() any { return actual.UID }) IntParamTest(t, "UAge", val, uint(1), 1, func() any { return actual.UAge }) IntParamTest(t, "UAge", val, nil, 0, func() any { return actual.UAge }) IntParamTest(t, "UVisits", val, uint32(1), 1, func() any { return actual.UVisits }) IntParamTest(t, "UVisits", val, nil, 0, func() any { return actual.UVisits }) IntParamTest(t, "UCount", val, uint16(1), 1, func() any { return actual.UCount }) IntParamTest(t, "UCount", val, nil, 0, func() any { return actual.UCount }) IntParamTest(t, "USeq", val, uint8(1), 1, func() any { return actual.USeq }) IntParamTest(t, "USeq", val, nil, 0, func() any { return actual.USeq }) FloatParamTest(t, "score", "Score", "float", val, 1.0, 1, func() any { return actual.Score }) FloatParamTest(t, "score", "Score", "float", val, nil, 0, func() any { return actual.Score }) FloatParamTest(t, "rate", "Rate", "double", val, 1.0, 1, func() any { return actual.Rate }) FloatParamTest(t, "rate", "Rate", "double", val, nil, 0, func() any { return actual.Rate }) pName = "Confirmed" confirmedField := val.FieldByName(pName) binder = &untypedParamBinder{ parameter: spec.QueryParam(pName).Typed("boolean", "").WithDefault(true), Name: pName, } for _, tv := range evaluatesAsTrue { err = binder.setFieldValue(confirmedField, true, tv, true) require.NoError(t, err) assert.TrueT(t, actual.Confirmed) } err = binder.setFieldValue(confirmedField, true, "", true) require.NoError(t, err) assert.TrueT(t, actual.Confirmed) err = binder.setFieldValue(confirmedField, true, "0", true) require.NoError(t, err) assert.FalseT(t, actual.Confirmed) pName = "Timestamp" timeField := val.FieldByName(pName) dt := strfmt.DateTime(time.Date(2014, 3, 19, 2, 9, 0, 0, time.UTC)) binder = &untypedParamBinder{ parameter: spec.QueryParam(pName).Typed(typeString, "date-time").WithDefault(dt), Name: pName, } exp := strfmt.DateTime(time.Date(2014, 5, 14, 2, 9, 0, 0, time.UTC)) err = binder.setFieldValue(timeField, dt, exp.String(), true) require.NoError(t, err) assert.Equal(t, exp, actual.Timestamp) err = binder.setFieldValue(timeField, dt, "", true) require.NoError(t, err) assert.Equal(t, dt, actual.Timestamp) err = binder.setFieldValue(timeField, dt, "yada", true) require.Error(t, err) ddt := strfmt.Date(time.Date(2014, 3, 19, 0, 0, 0, 0, time.UTC)) pName = "Birthdate" dateField := val.FieldByName(pName) binder = &untypedParamBinder{ parameter: spec.QueryParam(pName).Typed(typeString, "date").WithDefault(ddt), Name: pName, } expd := strfmt.Date(time.Date(2014, 5, 14, 0, 0, 0, 0, time.UTC)) err = binder.setFieldValue(dateField, ddt, expd.String(), true) require.NoError(t, err) assert.Equal(t, expd, actual.Birthdate) err = binder.setFieldValue(dateField, ddt, "", true) require.NoError(t, err) assert.Equal(t, ddt, actual.Birthdate) err = binder.setFieldValue(dateField, ddt, "yada", true) require.Error(t, err) dt = strfmt.DateTime(time.Date(2014, 3, 19, 2, 9, 0, 0, time.UTC)) fdt := &dt pName = "LastFailure" ftimeField := val.FieldByName(pName) binder = &untypedParamBinder{ parameter: spec.QueryParam(pName).Typed(typeString, "date").WithDefault(fdt), Name: pName, } exp = strfmt.DateTime(time.Date(2014, 5, 14, 2, 9, 0, 0, time.UTC)) fexp := &exp err = binder.setFieldValue(ftimeField, fdt, fexp.String(), true) require.NoError(t, err) assert.Equal(t, fexp, actual.LastFailure) err = binder.setFieldValue(ftimeField, fdt, "", true) require.NoError(t, err) assert.Equal(t, fdt, actual.LastFailure) err = binder.setFieldValue(ftimeField, fdt, "", true) require.NoError(t, err) assert.Equal(t, fdt, actual.LastFailure) actual.LastFailure = nil err = binder.setFieldValue(ftimeField, fdt, "yada", true) require.Error(t, err) assert.Nil(t, actual.LastFailure) pName = "Unsupported" unsupportedField := val.FieldByName(pName) binder = &untypedParamBinder{ parameter: spec.QueryParam(pName).Typed(typeString, ""), Name: pName, } err = binder.setFieldValue(unsupportedField, nil, "", true) require.Error(t, err) } func TestSliceConversion(t *testing.T) { actual := new(SomeOperationParams) val := reflect.ValueOf(actual).Elem() // prefsField := val.FieldByName("Prefs") // cData := "yada,2,3" // _, _, err := readFormattedSliceFieldValue("Prefs", prefsField, cData, "csv", nil) // require.Error(t, err) sliced := []string{"some", typeString, "values"} seps := map[string]string{ssvFmt: " ", tsvFmt: "\t", pipesFmt: "|", "csv": ",", "": ","} tagsField := val.FieldByName(keyTags) for k, sep := range seps { binder := &untypedParamBinder{ Name: keyTags, parameter: spec.QueryParam("tags").CollectionOf(stringItems, k), } actual.Tags = nil cData := strings.Join(sliced, sep) tags, _, err := binder.readFormattedSliceFieldValue(cData, tagsField) require.NoError(t, err) assert.Equal(t, sliced, tags) cData = strings.Join(sliced, " "+sep+" ") tags, _, err = binder.readFormattedSliceFieldValue(cData, tagsField) require.NoError(t, err) assert.Equal(t, sliced, tags) tags, _, err = binder.readFormattedSliceFieldValue("", tagsField) require.NoError(t, err) assert.Empty(t, tags) } assert.Nil(t, stringutils.SplitByFormat("yada", "multi")) assert.Nil(t, stringutils.SplitByFormat("", "")) categoriesField := val.FieldByName("Categories") binder := &untypedParamBinder{ Name: "Categories", parameter: spec.QueryParam("categories").CollectionOf(stringItems, "csv"), } cData := strings.Join(sliced, ",") categories, custom, err := binder.readFormattedSliceFieldValue(cData, categoriesField) require.NoError(t, err) assert.EqualValues(t, sliced, actual.Categories) assert.TrueT(t, custom) assert.Empty(t, categories) categories, custom, err = binder.readFormattedSliceFieldValue("", categoriesField) require.Error(t, err) assert.TrueT(t, custom) assert.Empty(t, categories) } go-openapi-runtime-decad8f/middleware/typeutils.go000066400000000000000000000017411520232310000225660ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import "strings" // normalizeOffer strips the parameter section (";...") from a media-type // string. func normalizeOffer(orig string) string { // NOTE(maintainers): Despite its name (kept for historical reasons), this helper is // not about Accept negotiation — it is used to derive the bare type that // keys the producer/consumer maps registered on a [RoutableAPI]. // Those maps are looked up by the bare media type, so an entry registered as // "application/json" satisfies a route that declares "application/json; // charset=utf-8" and vice-versa. const maxParts = 2 return strings.SplitN(orig, ";", maxParts)[0] } // normalizeOffers is the slice form of [normalizeOffer]. func normalizeOffers(orig []string) []string { norm := make([]string, 0, len(orig)) for _, o := range orig { norm = append(norm, normalizeOffer(o)) } return norm } go-openapi-runtime-decad8f/middleware/untyped/000077500000000000000000000000001520232310000216625ustar00rootroot00000000000000go-openapi-runtime-decad8f/middleware/untyped/api.go000066400000000000000000000175701520232310000227740ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package untyped import ( "fmt" "net/http" "sort" "strings" "github.com/go-openapi/analysis" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/runtime" ) const ( smallPreallocatedSlots = 10 mediumPreallocatedSlots = 30 ) // API represents an untyped mux for a swagger spec. type API struct { spec *loads.Document analyzer *analysis.Spec DefaultProduces string DefaultConsumes string consumers map[string]runtime.Consumer producers map[string]runtime.Producer authenticators map[string]runtime.Authenticator authorizer runtime.Authorizer operations map[string]map[string]runtime.OperationHandler ServeError func(http.ResponseWriter, *http.Request, error) Models map[string]func() any formats strfmt.Registry } // NewAPI creates the default untyped API. func NewAPI(spec *loads.Document) *API { var an *analysis.Spec if spec != nil && spec.Spec() != nil { an = analysis.New(spec.Spec()) } api := &API{ spec: spec, analyzer: an, consumers: make(map[string]runtime.Consumer, smallPreallocatedSlots), producers: make(map[string]runtime.Producer, smallPreallocatedSlots), authenticators: make(map[string]runtime.Authenticator), operations: make(map[string]map[string]runtime.OperationHandler), ServeError: errors.ServeError, Models: make(map[string]func() any), formats: strfmt.NewFormats(), } return api.WithJSONDefaults() } // WithJSONDefaults loads the json defaults for this api. func (d *API) WithJSONDefaults() *API { d.DefaultConsumes = runtime.JSONMime d.DefaultProduces = runtime.JSONMime d.consumers[runtime.JSONMime] = runtime.JSONConsumer() d.producers[runtime.JSONMime] = runtime.JSONProducer() return d } // WithoutJSONDefaults clears the json defaults for this api. func (d *API) WithoutJSONDefaults() *API { d.DefaultConsumes = "" d.DefaultProduces = "" delete(d.consumers, runtime.JSONMime) delete(d.producers, runtime.JSONMime) return d } // Formats returns the registered string formats. func (d *API) Formats() strfmt.Registry { if d.formats == nil { d.formats = strfmt.NewFormats() } return d.formats } // RegisterFormat registers a custom format validator. func (d *API) RegisterFormat(name string, format strfmt.Format, validator strfmt.Validator) { if d.formats == nil { d.formats = strfmt.NewFormats() } d.formats.Add(name, format, validator) } // RegisterAuth registers an auth handler in this api. func (d *API) RegisterAuth(scheme string, handler runtime.Authenticator) { if d.authenticators == nil { d.authenticators = make(map[string]runtime.Authenticator) } d.authenticators[scheme] = handler } // RegisterAuthorizer registers an authorizer handler in this api. func (d *API) RegisterAuthorizer(handler runtime.Authorizer) { d.authorizer = handler } // RegisterConsumer registers a consumer for a media type. func (d *API) RegisterConsumer(mediaType string, handler runtime.Consumer) { if d.consumers == nil { d.consumers = make(map[string]runtime.Consumer, smallPreallocatedSlots) } d.consumers[strings.ToLower(mediaType)] = handler } // RegisterProducer registers a producer for a media type. func (d *API) RegisterProducer(mediaType string, handler runtime.Producer) { if d.producers == nil { d.producers = make(map[string]runtime.Producer, smallPreallocatedSlots) } d.producers[strings.ToLower(mediaType)] = handler } // RegisterOperation registers an operation handler for an operation name. func (d *API) RegisterOperation(method, path string, handler runtime.OperationHandler) { if d.operations == nil { d.operations = make(map[string]map[string]runtime.OperationHandler, mediumPreallocatedSlots) } um := strings.ToUpper(method) if b, ok := d.operations[um]; !ok || b == nil { d.operations[um] = make(map[string]runtime.OperationHandler) } d.operations[um][path] = handler } // OperationHandlerFor returns the operation handler for the specified id if it can be found. func (d *API) OperationHandlerFor(method, path string) (runtime.OperationHandler, bool) { if d.operations == nil { return nil, false } if pi, ok := d.operations[strings.ToUpper(method)]; ok { h, ok := pi[path] return h, ok } return nil, false } // ConsumersFor gets the consumers for the specified media types. func (d *API) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer { result := make(map[string]runtime.Consumer) for _, mt := range mediaTypes { if consumer, ok := d.consumers[mt]; ok { result[mt] = consumer } } return result } // ProducersFor gets the producers for the specified media types. func (d *API) ProducersFor(mediaTypes []string) map[string]runtime.Producer { result := make(map[string]runtime.Producer) for _, mt := range mediaTypes { if producer, ok := d.producers[mt]; ok { result[mt] = producer } } return result } // AuthenticatorsFor gets the authenticators for the specified security schemes. func (d *API) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator { result := make(map[string]runtime.Authenticator) for k := range schemes { if a, ok := d.authenticators[k]; ok { result[k] = a } } return result } // Authorizer returns the registered authorizer. func (d *API) Authorizer() runtime.Authorizer { return d.authorizer } // Validate validates this API for any missing items. func (d *API) Validate() error { return d.validate() } // validateWith validates the registrations in this API against the provided spec analyzer. func (d *API) validate() error { consumes := make([]string, 0, len(d.consumers)) for k := range d.consumers { consumes = append(consumes, k) } produces := make([]string, 0, len(d.producers)) for k := range d.producers { produces = append(produces, k) } authenticators := make([]string, 0, len(d.authenticators)) for k := range d.authenticators { authenticators = append(authenticators, k) } operations := make([]string, 0, len(d.operations)) for m, v := range d.operations { for p := range v { operations = append(operations, fmt.Sprintf("%s %s", strings.ToUpper(m), p)) } } secDefinitions := d.spec.Spec().SecurityDefinitions definedAuths := make([]string, 0, len(secDefinitions)) for k := range secDefinitions { definedAuths = append(definedAuths, k) } if err := d.verify("consumes", consumes, d.analyzer.RequiredConsumes()); err != nil { return err } if err := d.verify("produces", produces, d.analyzer.RequiredProduces()); err != nil { return err } if err := d.verify("operation", operations, d.analyzer.OperationMethodPaths()); err != nil { return err } requiredAuths := d.analyzer.RequiredSecuritySchemes() if err := d.verify("auth scheme", authenticators, requiredAuths); err != nil { return err } if err := d.verify("security definitions", definedAuths, requiredAuths); err != nil { return err } return nil } func (d *API) verify(name string, registrations []string, expectations []string) error { sort.Strings(registrations) sort.Strings(expectations) expected := map[string]struct{}{} seen := map[string]struct{}{} for _, v := range expectations { expected[v] = struct{}{} } var unspecified []string for _, v := range registrations { seen[v] = struct{}{} if _, ok := expected[v]; !ok { unspecified = append(unspecified, v) } } for k := range seen { delete(expected, k) } unregistered := make([]string, 0, len(expected)) for k := range expected { unregistered = append(unregistered, k) } sort.Strings(unspecified) sort.Strings(unregistered) if len(unregistered) > 0 || len(unspecified) > 0 { return &errors.APIVerificationFailed{ Section: name, MissingSpecification: unspecified, MissingRegistration: unregistered, } } return nil } go-openapi-runtime-decad8f/middleware/untyped/api_test.go000066400000000000000000000174051520232310000240300ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package untyped import ( "io" "net/http" "sort" "testing" "github.com/go-openapi/analysis" "github.com/go-openapi/errors" "github.com/go-openapi/loads" "github.com/go-openapi/runtime" swaggerspec "github.com/go-openapi/spec" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) const jsonMime = "application/json" func stubAutenticator() runtime.Authenticator { return runtime.AuthenticatorFunc(func(_ any) (bool, any, error) { return false, nil, nil }) } func stubAuthorizer() runtime.Authorizer { return runtime.AuthorizerFunc(func(_ *http.Request, _ any) error { return nil }) } type stubConsumer struct { } func (s *stubConsumer) Consume(_ io.Reader, _ any) error { return nil } type stubProducer struct { } func (s *stubProducer) Produce(_ io.Writer, _ any) error { return nil } type stubOperationHandler struct { } func (s *stubOperationHandler) ParameterModel() any { return nil } func (s *stubOperationHandler) Handle(_ any) (any, error) { return map[string]any{}, nil } func TestUntypedAPIRegistrations(t *testing.T) { api := NewAPI(new(loads.Document)).WithJSONDefaults() api.RegisterConsumer("application/yada", new(stubConsumer)) api.RegisterProducer("application/yada-2", new(stubProducer)) api.RegisterOperation("get", "/{someId}", new(stubOperationHandler)) api.RegisterAuth("basic", stubAutenticator()) api.RegisterAuthorizer(stubAuthorizer()) assert.NotNil(t, api.authorizer) assert.NotEmpty(t, api.authenticators) _, ok := api.authenticators["basic"] assert.TrueT(t, ok) _, ok = api.consumers["application/yada"] assert.TrueT(t, ok) _, ok = api.producers["application/yada-2"] assert.TrueT(t, ok) _, ok = api.consumers[jsonMime] assert.TrueT(t, ok) _, ok = api.producers[jsonMime] assert.TrueT(t, ok) _, ok = api.operations["GET"]["/{someId}"] assert.TrueT(t, ok) authorizer := api.Authorizer() assert.NotNil(t, authorizer) h, ok := api.OperationHandlerFor("get", "/{someId}") assert.TrueT(t, ok) assert.NotNil(t, h) _, ok = api.OperationHandlerFor("doesntExist", "/{someId}") assert.FalseT(t, ok) } func TestUntypedAppValidation(t *testing.T) { invalidSpecStr := `{ "consumes": ["application/json"], "produces": ["application/json"], "security": [ {"apiKey":[]} ], "parameters": { "format": { "in": "query", "name": "format", "type": "string" } }, "paths": { "/": { "parameters": [ { "name": "limit", "type": "integer", "format": "int32", "x-go-name": "Limit" } ], "get": { "consumes": ["application/x-yaml"], "produces": ["application/x-yaml"], "security": [ {"basic":[]} ], "parameters": [ { "name": "skip", "type": "integer", "format": "int32" } ] } } } }` specStr := `{ "consumes": ["application/json"], "produces": ["application/json"], "security": [ {"apiKey":[]} ], "securityDefinitions": { "basic": { "type": "basic" }, "apiKey": { "type": "apiKey", "in":"header", "name":"X-API-KEY" } }, "parameters": { "format": { "in": "query", "name": "format", "type": "string" } }, "paths": { "/": { "parameters": [ { "name": "limit", "type": "integer", "format": "int32", "x-go-name": "Limit" } ], "get": { "consumes": ["application/x-yaml"], "produces": ["application/x-yaml"], "security": [ {"basic":[]} ], "parameters": [ { "name": "skip", "type": "integer", "format": "int32" } ] } } } }` validSpec, err := loads.Analyzed([]byte(specStr), "") require.NoError(t, err) assert.NotNil(t, validSpec) spec, err := loads.Analyzed([]byte(invalidSpecStr), "") require.NoError(t, err) assert.NotNil(t, spec) analyzed := analysis.New(spec.Spec()) analyzedValid := analysis.New(validSpec.Spec()) cons := analyzed.ConsumesFor(analyzed.AllPaths()["/"].Get) assert.Len(t, cons, 1) prods := analyzed.RequiredProduces() assert.Len(t, prods, 2) api1 := NewAPI(spec) err = api1.Validate() require.Error(t, err) assert.EqualT(t, "missing [application/x-yaml] consumes registrations", err.Error()) api1.RegisterConsumer("application/x-yaml", new(stubConsumer)) err = api1.validate() require.Error(t, err) assert.EqualT(t, "missing [application/x-yaml] produces registrations", err.Error()) api1.RegisterProducer("application/x-yaml", new(stubProducer)) err = api1.validate() require.Error(t, err) assert.EqualT(t, "missing [GET /] operation registrations", err.Error()) api1.RegisterOperation("get", "/", new(stubOperationHandler)) err = api1.validate() require.Error(t, err) assert.EqualT(t, "missing [apiKey, basic] auth scheme registrations", err.Error()) api1.RegisterAuth("basic", stubAutenticator()) api1.RegisterAuth("apiKey", stubAutenticator()) err = api1.validate() require.Error(t, err) assert.EqualT(t, "missing [apiKey, basic] security definitions registrations", err.Error()) api3 := NewAPI(validSpec) api3.RegisterConsumer("application/x-yaml", new(stubConsumer)) api3.RegisterProducer("application/x-yaml", new(stubProducer)) api3.RegisterOperation("get", "/", new(stubOperationHandler)) api3.RegisterAuth("basic", stubAutenticator()) api3.RegisterAuth("apiKey", stubAutenticator()) err = api3.validate() require.NoError(t, err) api3.RegisterConsumer("application/something", new(stubConsumer)) err = api3.validate() require.Error(t, err) assert.EqualT(t, "missing from spec file [application/something] consumes", err.Error()) api2 := NewAPI(spec) api2.RegisterConsumer("application/something", new(stubConsumer)) err = api2.validate() require.Error(t, err) assert.EqualT(t, "missing [application/x-yaml] consumes registrations\nmissing from spec file [application/something] consumes", err.Error()) api2.RegisterConsumer("application/x-yaml", new(stubConsumer)) delete(api2.consumers, "application/something") api2.RegisterProducer("application/something", new(stubProducer)) err = api2.validate() require.Error(t, err) assert.EqualT(t, "missing [application/x-yaml] produces registrations\nmissing from spec file [application/something] produces", err.Error()) delete(api2.producers, "application/something") api2.RegisterProducer("application/x-yaml", new(stubProducer)) expected := []string{"application/x-yaml"} sort.Strings(expected) consumes := analyzed.ConsumesFor(analyzed.AllPaths()["/"].Get) sort.Strings(consumes) assert.Equal(t, expected, consumes) consumers := api1.ConsumersFor(consumes) assert.Len(t, consumers, 1) produces := analyzed.ProducesFor(analyzed.AllPaths()["/"].Get) sort.Strings(produces) assert.Equal(t, expected, produces) producers := api1.ProducersFor(produces) assert.Len(t, producers, 1) definitions := analyzedValid.SecurityDefinitionsFor(analyzedValid.AllPaths()["/"].Get) expectedSchemes := map[string]swaggerspec.SecurityScheme{"basic": *swaggerspec.BasicAuth()} assert.Equal(t, expectedSchemes, definitions) authenticators := api3.AuthenticatorsFor(definitions) assert.Len(t, authenticators, 1) opHandler := runtime.OperationHandlerFunc(func(data any) (any, error) { return data, nil }) d, err := opHandler.Handle(1) require.NoError(t, err) assert.Equal(t, 1, d) authenticator := runtime.AuthenticatorFunc(func(params any) (bool, any, error) { if str, ok := params.(string); ok { return ok, str, nil } return true, nil, errors.Unauthenticated("authenticator") }) ok, p, err := authenticator.Authenticate("hello") assert.TrueT(t, ok) require.NoError(t, err) assert.Equal(t, "hello", p) } go-openapi-runtime-decad8f/middleware/untyped_request_test.go000066400000000000000000000174531520232310000250320ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "bytes" "context" "encoding/base64" "encoding/json" "io" "mime/multipart" "net/http" "net/url" "strings" "testing" "time" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestUntypedFormPost(t *testing.T) { params := parametersForFormUpload() binder := NewUntypedRequestBinder(params, nil, strfmt.Default) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, bytes.NewBufferString(`name=the-name&age=32`)) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") data := make(map[string]any) require.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) assert.Equal(t, "the-name", data[paramKeyName]) assert.EqualValues(t, 32, data[paramKeyAge]) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, bytes.NewBufferString(`name=%3&age=32`)) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") data = make(map[string]any) require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) } func TestUntypedFileUpload(t *testing.T) { binder := paramsForFileUpload() body := bytes.NewBuffer(nil) writer := multipart.NewWriter(body) part, err := writer.CreateFormFile("file", "plain-jane.txt") require.NoError(t, err) _, err = part.Write([]byte("the file contents")) require.NoError(t, err) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) data := make(map[string]any) require.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) assert.Equal(t, "the-name", data[paramKeyName]) assert.NotNil(t, data["file"]) assert.IsType(t, runtime.File{}, data["file"]) file, ok := data["file"].(runtime.File) require.TrueT(t, ok) require.NotNil(t, file.Header) assert.EqualT(t, "plain-jane.txt", file.Header.Filename) bb, err := io.ReadAll(file.Data) require.NoError(t, err) assert.Equal(t, []byte("the file contents"), bb) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, body) require.NoError(t, err) req.Header.Set("Content-Type", jsonMime) data = make(map[string]any) require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, body) require.NoError(t, err) req.Header.Set("Content-Type", "application(") data = make(map[string]any) require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) body = bytes.NewBuffer(nil) writer = multipart.NewWriter(body) part, err = writer.CreateFormFile("bad-name", "plain-jane.txt") require.NoError(t, err) _, err = part.Write([]byte("the file contents")) require.NoError(t, err) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) data = make(map[string]any) require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) _, err = req.MultipartReader() require.NoError(t, err) data = make(map[string]any) require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) writer = multipart.NewWriter(body) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) data = make(map[string]any) require.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) } func TestUntypedOptionalFileUpload(t *testing.T) { binder := paramsForOptionalFileUpload() body := bytes.NewBuffer(nil) writer := multipart.NewWriter(body) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) data := make(map[string]any) require.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) assert.Equal(t, "the-name", data[paramKeyName]) writer = multipart.NewWriter(body) part, err := writer.CreateFormFile("file", "plain-jane.txt") require.NoError(t, err) _, err = part.Write([]byte("the file contents")) require.NoError(t, err) require.NoError(t, writer.WriteField(paramKeyName, "the-name")) require.NoError(t, writer.Close()) req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, testURL, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) require.NoError(t, writer.Close()) data = make(map[string]any) require.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data)) assert.Equal(t, "the-name", data[paramKeyName]) assert.NotNil(t, data["file"]) assert.IsType(t, runtime.File{}, data["file"]) file, ok := data["file"].(runtime.File) require.TrueT(t, ok) assert.NotNil(t, file.Header) assert.EqualT(t, "plain-jane.txt", file.Header.Filename) } func TestUntypedBindingTypesForValid(t *testing.T) { op2 := parametersForAllTypes("") binder := NewUntypedRequestBinder(op2, nil, strfmt.Default) confirmed := true name := "thomas" friend := map[string]any{paramKeyName: valToby, paramKeyAge: json.Number("32")} id, age, score, factor := int64(7575), int32(348), float32(5.309), float64(37.403) requestID := 19394858 tags := []string{tagOne, tagTwo, tagThree} dt1 := time.Date(2014, 8, 9, 0, 0, 0, 0, time.UTC) planned := strfmt.Date(dt1) dt2 := time.Date(2014, 10, 12, 8, 5, 5, 0, time.UTC) delivered := strfmt.DateTime(dt2) picture := base64.URLEncoding.EncodeToString([]byte("hello")) uri, err := url.Parse("http://localhost:8002/hello/7575") require.NoError(t, err) qs := uri.Query() qs.Add(paramKeyName, name) qs.Add("confirmed", "true") qs.Add(paramKeyAge, "348") qs.Add("score", "5.309") qs.Add("factor", "37.403") qs.Add("tags", strings.Join(tags, ",")) qs.Add("planned", planned.String()) qs.Add("delivered", delivered.String()) qs.Add("picture", picture) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, uri.String()+"?"+qs.Encode(), bytes.NewBufferString(`{"name":"toby","age":32}`)) require.NoError(t, err) req.Header.Set("Content-Type", jsonMime) req.Header.Set("X-Request-Id", "19394858") data := make(map[string]any) err = binder.Bind(req, RouteParams([]RouteParam{{paramKeyID, "7575"}}), runtime.JSONConsumer(), &data) require.NoError(t, err) assert.Equal(t, id, data[paramKeyID]) assert.Equal(t, name, data[paramKeyName]) assert.Equal(t, friend, data["friend"]) assert.EqualValues(t, requestID, data["X-Request-Id"]) assert.Equal(t, tags, data["tags"]) assert.Equal(t, planned, data["planned"]) assert.Equal(t, delivered, data["delivered"]) assert.Equal(t, confirmed, data["confirmed"]) assert.Equal(t, age, data[paramKeyAge]) assert.InDelta(t, factor, data["factor"], 1e-6) assert.InDelta(t, score, data["score"], 1e-6) pb, err := base64.URLEncoding.DecodeString(picture) require.NoError(t, err) formatted, ok := data["picture"].(strfmt.Base64) require.TrueT(t, ok) assert.EqualValues(t, pb, formatted) } go-openapi-runtime-decad8f/middleware/validation.go000066400000000000000000000074301520232310000226570ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( stderrors "errors" "net/http" "strings" "github.com/go-openapi/errors" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/server-middleware/mediatype" ) type validation struct { context *Context result []error request *http.Request route *MatchedRoute bound map[string]any } // validateContentType maps [mediatype.MatchFirst] to the runtime's // validation errors: // // - actual fails to parse → HTTP 400 ([errors.NewParseError]). // - actual is well-formed but // no allowed entry accepts it → HTTP 415 ([errors.InvalidContentType]). // // In the standard runtime flow, malformed Content-Type headers are // already caught upstream by [runtime.ContentType] (which itself returns // a 400 [errors.ParseError]). This function therefore only sees the // malformed case when invoked directly by callers that have bypassed // that step. func validateContentType(allowed []string, actual string, opts ...mediatype.MatchOption) error { if len(allowed) == 0 { return nil } _, ok, err := mediatype.MatchFirst(allowed, actual, opts...) if ok { return nil } if err != nil { return errors.NewParseError(runtime.HeaderContentType, "header", actual, err) } return errors.InvalidContentType(actual, allowed) } func validateRequest(ctx *Context, request *http.Request, route *MatchedRoute) *validation { validate := &validation{ context: ctx, request: request, route: route, bound: make(map[string]any), } validate.debugLogf("validating request %s %s", request.Method, request.URL.EscapedPath()) validate.contentType() if len(validate.result) == 0 { validate.responseFormat() } if len(validate.result) == 0 { validate.parameters() } return validate } func (v *validation) debugLogf(format string, args ...any) { v.context.debugLogf(format, args...) } func (v *validation) parameters() { v.debugLogf("validating request parameters for %s %s", v.request.Method, v.request.URL.EscapedPath()) result := v.route.Binder.bind(v.request, v.route.Params, v.route.Consumer, v.bound) if result == nil { return } for _, e := range result.Errors { var validationErr *errors.Validation if stderrors.As(e, &validationErr) { v.result = append(v.result, validationErr) } } } func (v *validation) contentType() { if len(v.result) > 0 || !runtime.HasBody(v.request) { return } v.debugLogf("validating body content type for %s %s", v.request.Method, v.request.URL.EscapedPath()) ct, _, req, err := v.context.ContentType(v.request) if err != nil { v.result = append(v.result, err) } else { v.request = req } if len(v.result) == 0 { v.debugLogf("validating content type for %q against [%s]", ct, strings.Join(v.route.Consumes, ", ")) if err := validateContentType(v.route.Consumes, ct, v.context.matchOpts()...); err != nil { v.result = append(v.result, err) } } if ct == "" || v.route.Consumer != nil { return } cons, ok := mediatype.Lookup(v.route.Consumers, ct, v.context.matchOpts()...) if !ok { v.result = append(v.result, errors.New(http.StatusInternalServerError, "no consumer registered for %s", ct)) } else { v.route.Consumer = cons } } func (v *validation) responseFormat() { // if the route provides values for Produces and no format could be identified then return an error. // if the route does not specify values for Produces then treat request as valid since the API designer // choose not to specify the format for responses. if str, rCtx := v.context.ResponseFormat(v.request, v.route.Produces); str == "" && len(v.route.Produces) > 0 { v.request = rCtx v.result = append(v.result, errors.InvalidResponseFormat(v.request.Header.Get(runtime.HeaderAccept), v.route.Produces)) } } go-openapi-runtime-decad8f/middleware/validation_test.go000066400000000000000000000141401520232310000237120ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package middleware import ( "bytes" stdcontext "context" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-openapi/errors" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/internal/testing/petstore" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func newTestValidation(ctx *Context, next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { matched, rCtx, _ := ctx.RouteInfo(r) if rCtx != nil { r = rCtx } if matched == nil { ctx.NotFound(rw, r) return } _, r, result := ctx.BindAndValidate(r, matched) if result != nil { ctx.Respond(rw, r, matched.Produces, matched, result) return } next.ServeHTTP(rw, r) }) } func TestContentTypeValidation(t *testing.T) { spec, api := petstore.NewAPI(t) context := NewContext(spec, api, nil) context.router = DefaultRouter(spec, context.api) mw := newTestValidation(context, http.HandlerFunc(terminator)) recorder := httptest.NewRecorder() request, _ := http.NewRequestWithContext(stdcontext.Background(), http.MethodGet, "/api/pets", nil) request.Header.Add("Accept", "*/*") mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) recorder = httptest.NewRecorder() request, _ = http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", nil) request.Header.Add("Content-Type", "application(") request.Header.Add("Accept", jsonMime) request.ContentLength = 1 mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusBadRequest, recorder.Code) assert.EqualT(t, jsonMime, recorder.Header().Get("Content-Type")) recorder = httptest.NewRecorder() request, _ = http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", nil) request.Header.Add("Accept", jsonMime) request.Header.Add("Content-Type", "text/html") request.ContentLength = 1 mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusUnsupportedMediaType, recorder.Code) assert.EqualT(t, jsonMime, recorder.Header().Get("Content-Type")) recorder = httptest.NewRecorder() request, _ = http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", strings.NewReader(`{"name":"dog"}`)) request.Header.Add("Accept", jsonMime) request.Header.Add("Content-Type", "text/html") request.TransferEncoding = []string{"chunked"} mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusUnsupportedMediaType, recorder.Code) assert.EqualT(t, jsonMime, recorder.Header().Get("Content-Type")) recorder = httptest.NewRecorder() request, _ = http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", nil) request.Header.Add("Accept", "application/json+special") request.Header.Add("Content-Type", "text/html") mw.ServeHTTP(recorder, request) assert.EqualT(t, 406, recorder.Code) assert.EqualT(t, jsonMime, recorder.Header().Get("Content-Type")) // client sends data with unsupported mime recorder = httptest.NewRecorder() request, _ = http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", nil) request.Header.Add("Accept", jsonMime) // this content type is served by default by the API request.Header.Add("Content-Type", "application/json+special") request.ContentLength = 1 mw.ServeHTTP(recorder, request) assert.EqualT(t, 415, recorder.Code) // Unsupported media type assert.EqualT(t, jsonMime, recorder.Header().Get("Content-Type")) // client sends a body of data with no mime: breaks recorder = httptest.NewRecorder() request, _ = http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", nil) request.Header.Add("Accept", jsonMime) request.ContentLength = 1 mw.ServeHTTP(recorder, request) assert.EqualT(t, 415, recorder.Code) assert.EqualT(t, jsonMime, recorder.Header().Get("Content-Type")) } func TestResponseFormatValidation(t *testing.T) { spec, api := petstore.NewAPI(t) context := NewContext(spec, api, nil) context.router = DefaultRouter(spec, context.api) mw := newTestValidation(context, http.HandlerFunc(terminator)) recorder := httptest.NewRecorder() request, _ := http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", bytes.NewBufferString(`name: Dog`)) request.Header.Set(runtime.HeaderContentType, "application/x-yaml") request.Header.Set(runtime.HeaderAccept, "application/x-yaml") mw.ServeHTTP(recorder, request) assert.EqualT(t, 200, recorder.Code, recorder.Body.String()) recorder = httptest.NewRecorder() request, _ = http.NewRequestWithContext(stdcontext.Background(), http.MethodPost, "/api/pets", bytes.NewBufferString(`name: Dog`)) request.Header.Set(runtime.HeaderContentType, "application/x-yaml") request.Header.Set(runtime.HeaderAccept, "application/sml") mw.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusNotAcceptable, recorder.Code) } // TestValidateContentType is a smoke test confirming the wrapper maps // no-match to 415 (errors.InvalidContentType) and malformed-actual to 400 // (errors.NewParseError). The matching matrix lives in // server-middleware/mediatype/match_test.go. func TestValidateContentType(t *testing.T) { t.Run("nil allowed accepts anything", func(t *testing.T) { require.NoError(t, validateContentType(nil, jsonMime)) }) t.Run("match returns nil", func(t *testing.T) { require.NoError(t, validateContentType([]string{jsonMime}, jsonMime)) }) t.Run("no match returns 415", func(t *testing.T) { err := validateContentType([]string{jsonMime}, "text/html") require.Error(t, err) var v *errors.Validation require.ErrorAs(t, err, &v) assert.EqualT(t, http.StatusUnsupportedMediaType, int(v.Code())) }) t.Run("malformed actual returns 400", func(t *testing.T) { // In the normal runtime flow this case is caught upstream by // runtime.ContentType. The smoke test exercises the direct path. err := validateContentType([]string{jsonMime}, "application(") require.Error(t, err) var p *errors.ParseError require.ErrorAs(t, err, &p) assert.EqualT(t, http.StatusBadRequest, int(p.Code())) }) } go-openapi-runtime-decad8f/request.go000066400000000000000000000061511520232310000200770ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "bufio" "context" "errors" "io" "net/http" "strings" "github.com/go-openapi/swag/stringutils" ) // CanHaveBody returns true if this method can have a body. func CanHaveBody(method string) bool { mn := strings.ToUpper(method) return mn == "POST" || mn == "PUT" || mn == "PATCH" || mn == "DELETE" } // IsSafe returns true if this is a request with a safe method. func IsSafe(r *http.Request) bool { mn := strings.ToUpper(r.Method) return mn == "GET" || mn == "HEAD" } // AllowsBody returns true if the request allows for a body. func AllowsBody(r *http.Request) bool { mn := strings.ToUpper(r.Method) return mn != "HEAD" } // HasBody returns true if this method needs a content-type. func HasBody(r *http.Request) bool { // happy case: we have a content length set if r.ContentLength > 0 { return true } if r.Header.Get("Content-Length") != "" { // in this case, no Transfer-Encoding should be present // we have a header set but it was explicitly set to 0, so we assume no body return false } rdr := newPeekingReader(r.Body) r.Body = rdr return rdr.HasContent() } func newPeekingReader(r io.ReadCloser) *peekingReader { if r == nil { return nil } return &peekingReader{ underlying: bufio.NewReader(r), orig: r, } } type peekingReader struct { underlying interface { Buffered() int Peek(int) ([]byte, error) Read([]byte) (int, error) } orig io.ReadCloser } func (p *peekingReader) HasContent() bool { if p == nil { return false } if p.underlying.Buffered() > 0 { return true } b, err := p.underlying.Peek(1) if err != nil { return false } return len(b) > 0 } func (p *peekingReader) Read(d []byte) (int, error) { if p == nil { return 0, io.EOF } if p.underlying == nil { return 0, io.ErrUnexpectedEOF } return p.underlying.Read(d) } func (p *peekingReader) Close() error { if p.underlying == nil { return errors.New("reader already closed") } p.underlying = nil if p.orig != nil { return p.orig.Close() } return nil } // JSONRequest creates a new [http] request with json headers set. // // It uses [context.Background]. func JSONRequest(method, urlStr string, body io.Reader) (*http.Request, error) { req, err := http.NewRequestWithContext(context.Background(), method, urlStr, body) if err != nil { return nil, err } req.Header.Add(HeaderContentType, JSONMime) req.Header.Add(HeaderAccept, JSONMime) return req, nil } // Gettable for things with a method [GetOK](string) (data string, hasKey bool, hasValue bool). type Gettable interface { GetOK(string) ([]string, bool, bool) } // ReadSingleValue reads a single value from the source. func ReadSingleValue(values Gettable, name string) string { vv, _, hv := values.GetOK(name) if hv { return vv[len(vv)-1] } return "" } // ReadCollectionValue reads a collection value from a string data source. func ReadCollectionValue(values Gettable, name, collectionFormat string) []string { v := ReadSingleValue(values, name) return stringutils.SplitByFormat(v, collectionFormat) } go-openapi-runtime-decad8f/request_test.go000066400000000000000000000130541520232310000211360ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package runtime import ( "bufio" "bytes" "context" "io" "net/http" "net/url" "strings" "testing" "github.com/go-openapi/testify/v2/require" "github.com/go-openapi/testify/v2/assert" ) type eofReader struct{} func (e *eofReader) Read(_ []byte) (int, error) { return 0, io.EOF } func closeReader(rdr io.Reader) *closeCounting { return &closeCounting{ rdr: rdr, } } type closeCounting struct { rdr io.Reader closed int } func (c *closeCounting) Read(d []byte) (int, error) { return c.rdr.Read(d) } func (c *closeCounting) Close() error { c.closed++ if cr, ok := c.rdr.(io.ReadCloser); ok { return cr.Close() } return nil } type countingBufioReader struct { buffereds int peeks int reads int br interface { Buffered() int Peek(int) ([]byte, error) Read([]byte) (int, error) } } func (c *countingBufioReader) Buffered() int { c.buffereds++ return c.br.Buffered() } func (c *countingBufioReader) Peek(v int) ([]byte, error) { c.peeks++ return c.br.Peek(v) } func (c *countingBufioReader) Read(p []byte) (int, error) { c.reads++ return c.br.Read(p) } func TestPeekingReader(t *testing.T) { // just passes to original reader when nothing called exp1 := []byte("original") pr1 := newPeekingReader(closeReader(bytes.NewReader(exp1))) b1, err := io.ReadAll(pr1) require.NoError(t, err) assert.Equal(t, exp1, b1) // uses actual when there was some buffering exp2 := []byte("actual") pr2 := newPeekingReader(closeReader(bytes.NewReader(exp2))) peeked, err := pr2.underlying.Peek(1) require.NoError(t, err) require.EqualT(t, "a", string(peeked)) b2, err := io.ReadAll(pr2) require.NoError(t, err) assert.EqualT(t, string(exp2), string(b2)) // passes close call through to original reader cr := closeReader(closeReader(bytes.NewReader(exp2))) pr3 := newPeekingReader(cr) require.NoError(t, pr3.Close()) require.EqualT(t, 1, cr.closed) // returns false when the stream is empty pr4 := newPeekingReader(closeReader(&eofReader{})) require.FalseT(t, pr4.HasContent()) // returns true when the stream has content rdr := closeReader(strings.NewReader("hello")) pr := newPeekingReader(rdr) cbr := &countingBufioReader{ br: bufio.NewReader(rdr), } pr.underlying = cbr require.TrueT(t, pr.HasContent()) require.EqualT(t, 1, cbr.buffereds) require.EqualT(t, 1, cbr.peeks) require.EqualT(t, 0, cbr.reads) require.TrueT(t, pr.HasContent()) require.EqualT(t, 2, cbr.buffereds) require.EqualT(t, 1, cbr.peeks) require.EqualT(t, 0, cbr.reads) b, err := io.ReadAll(pr) require.NoError(t, err) require.EqualT(t, "hello", string(b)) require.EqualT(t, 2, cbr.buffereds) require.EqualT(t, 1, cbr.peeks) require.EqualT(t, 2, cbr.reads) require.EqualT(t, 0, cbr.br.Buffered()) t.Run("closing a closed peekingReader", func(t *testing.T) { const content = "content" r := newPeekingReader(io.NopCloser(strings.NewReader(content))) require.NoError(t, r.Close()) require.NotPanics(t, func() { err := r.Close() require.Error(t, err) }) }) t.Run("reading from a closed peekingReader", func(t *testing.T) { const content = "content" r := newPeekingReader(io.NopCloser(strings.NewReader(content))) require.NoError(t, r.Close()) require.NotPanics(t, func() { _, err := io.ReadAll(r) require.Error(t, err) require.ErrorIs(t, err, io.ErrUnexpectedEOF) }) }) t.Run("reading from a nil peekingReader", func(t *testing.T) { var r *peekingReader require.NotPanics(t, func() { buf := make([]byte, 10) _, err := r.Read(buf) require.Error(t, err) require.ErrorIs(t, err, io.EOF) }) }) } func TestJSONRequest(t *testing.T) { req, err := JSONRequest(http.MethodGet, "/swagger.json", nil) require.NoError(t, err) assert.EqualT(t, http.MethodGet, req.Method) assert.EqualT(t, JSONMime, req.Header.Get(HeaderContentType)) assert.EqualT(t, JSONMime, req.Header.Get(HeaderAccept)) req, err = JSONRequest(http.MethodGet, "%2", nil) require.Error(t, err) assert.Nil(t, req) } func TestHasBody(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) require.NoError(t, err) assert.FalseT(t, HasBody(req)) req.ContentLength = 123 assert.TrueT(t, HasBody(req)) } func TestMethod(t *testing.T) { testcase := []struct { method string canHaveBody bool allowsBody bool isSafe bool }{ {"put", true, true, false}, {"post", true, true, false}, {"patch", true, true, false}, {"delete", true, true, false}, {"get", false, true, true}, {"options", false, true, false}, {"head", false, false, true}, {"invalid", false, true, false}, {"", false, true, false}, } for _, tc := range testcase { t.Run(tc.method, func(t *testing.T) { assert.EqualT(t, tc.canHaveBody, CanHaveBody(tc.method), "CanHaveBody") req := http.Request{Method: tc.method} assert.EqualT(t, tc.allowsBody, AllowsBody(&req), "AllowsBody") assert.EqualT(t, tc.isSafe, IsSafe(&req), "IsSafe") }) } } func TestReadSingle(t *testing.T) { values := url.Values(make(map[string][]string)) values.Add("something", "the thing") assert.EqualT(t, "the thing", ReadSingleValue(Values(values), "something")) assert.Empty(t, ReadSingleValue(Values(values), "notthere")) } func TestReadCollection(t *testing.T) { values := url.Values(make(map[string][]string)) values.Add("something", "value1,value2") assert.Equal(t, []string{valValue1, valValue2}, ReadCollectionValue(Values(values), "something", "csv")) assert.Empty(t, ReadCollectionValue(Values(values), "notthere", "")) } go-openapi-runtime-decad8f/security/000077500000000000000000000000001520232310000177245ustar00rootroot00000000000000go-openapi-runtime-decad8f/security/apikey_auth_test.go000066400000000000000000000152641520232310000236250ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package security import ( "context" "fmt" "net/http" "testing" "github.com/go-openapi/errors" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) const ( apiKeyParam = "api_key" apiKeyHeader = "X-Api-Key" //nolint:gosec ) func TestApiKeyAuth(t *testing.T) { tokenAuth := TokenAuthentication(func(token string) (any, error) { if token == validToken { return principal, nil } return nil, errors.Unauthenticated("token") }) t.Run("with invalid initialization", func(t *testing.T) { assert.Panics(t, func() { APIKeyAuth(apiKeyParam, "qery", tokenAuth) }) }) t.Run("with token in query param", func(t *testing.T) { ta := APIKeyAuth(apiKeyParam, query, tokenAuth) t.Run("with valid token", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("%s?%s=%s", authPath, apiKeyParam, validToken), nil) require.NoError(t, err) ok, usr, err := ta.Authenticate(req) assert.TrueT(t, ok) assert.Equal(t, principal, usr) require.NoError(t, err) }) t.Run("with invalid token", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("%s?%s=%s", authPath, apiKeyParam, invalidToken), nil) require.NoError(t, err) ok, usr, err := ta.Authenticate(req) assert.TrueT(t, ok) assert.Nil(t, usr) require.Error(t, err) }) t.Run("with missing token", func(t *testing.T) { // put the token in the header, but query param is expected req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, authPath, nil) require.NoError(t, err) req.Header.Set(apiKeyHeader, validToken) ok, usr, err := ta.Authenticate(req) assert.FalseT(t, ok) assert.Nil(t, usr) require.NoError(t, err) }) }) t.Run("with token in header", func(t *testing.T) { ta := APIKeyAuth(apiKeyHeader, header, tokenAuth) t.Run("with valid token", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, authPath, nil) require.NoError(t, err) req.Header.Set(apiKeyHeader, validToken) ok, usr, err := ta.Authenticate(req) assert.TrueT(t, ok) assert.Equal(t, principal, usr) require.NoError(t, err) }) t.Run("with invalid token", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, authPath, nil) require.NoError(t, err) req.Header.Set(apiKeyHeader, invalidToken) ok, usr, err := ta.Authenticate(req) assert.TrueT(t, ok) assert.Nil(t, usr) require.Error(t, err) }) t.Run("with missing token", func(t *testing.T) { // put the token in the query param, but header is expected req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("%s?%s=%s", authPath, apiKeyParam, validToken), nil) require.NoError(t, err) ok, usr, err := ta.Authenticate(req) assert.FalseT(t, ok) assert.Nil(t, usr) require.NoError(t, err) }) }) } func TestApiKeyAuthCtx(t *testing.T) { tokenAuthCtx := TokenAuthenticationCtx(func(ctx context.Context, token string) (context.Context, any, error) { if token == validToken { return context.WithValue(ctx, extra, extraWisdom), principal, nil } return context.WithValue(ctx, reason, expReason), nil, errors.Unauthenticated("token") }) ctx := context.WithValue(context.Background(), original, wisdom) t.Run("with invalid initialization", func(t *testing.T) { assert.Panics(t, func() { APIKeyAuthCtx(apiKeyParam, "qery", tokenAuthCtx) }) }) t.Run("with token in query param", func(t *testing.T) { ta := APIKeyAuthCtx(apiKeyParam, query, tokenAuthCtx) t.Run("with valid token", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s?%s=%s", authPath, apiKeyParam, validToken), nil) require.NoError(t, err) ok, usr, err := ta.Authenticate(req) assert.TrueT(t, ok) assert.Equal(t, principal, usr) require.NoError(t, err) assert.Equal(t, wisdom, req.Context().Value(original)) assert.Equal(t, extraWisdom, req.Context().Value(extra)) assert.Nil(t, req.Context().Value(reason)) }) t.Run("with invalid token", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s?%s=%s", authPath, apiKeyParam, invalidToken), nil) require.NoError(t, err) ok, usr, err := ta.Authenticate(req) assert.TrueT(t, ok) assert.Nil(t, usr) require.Error(t, err) assert.Equal(t, wisdom, req.Context().Value(original)) assert.Equal(t, expReason, req.Context().Value(reason)) assert.Nil(t, req.Context().Value(extra)) }) t.Run("with missing token", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, authPath, nil) require.NoError(t, err) req.Header.Set(apiKeyHeader, validToken) ok, usr, err := ta.Authenticate(req) assert.FalseT(t, ok) assert.Nil(t, usr) require.NoError(t, err) assert.Equal(t, wisdom, req.Context().Value(original)) assert.Nil(t, req.Context().Value(reason)) assert.Nil(t, req.Context().Value(extra)) }) }) t.Run("with token in header", func(t *testing.T) { ta := APIKeyAuthCtx(apiKeyHeader, header, tokenAuthCtx) t.Run("with valid token", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, authPath, nil) require.NoError(t, err) req.Header.Set(apiKeyHeader, validToken) ok, usr, err := ta.Authenticate(req) assert.TrueT(t, ok) assert.Equal(t, principal, usr) require.NoError(t, err) assert.Equal(t, wisdom, req.Context().Value(original)) assert.Equal(t, extraWisdom, req.Context().Value(extra)) assert.Nil(t, req.Context().Value(reason)) }) t.Run("with invalid token", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, authPath, nil) require.NoError(t, err) req.Header.Set(apiKeyHeader, invalidToken) ok, usr, err := ta.Authenticate(req) assert.TrueT(t, ok) assert.Nil(t, usr) require.Error(t, err) assert.Equal(t, wisdom, req.Context().Value(original)) assert.Equal(t, expReason, req.Context().Value(reason)) assert.Nil(t, req.Context().Value(extra)) }) t.Run("with missing token", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s?%s=%s", authPath, apiKeyParam, validToken), nil) require.NoError(t, err) ok, usr, err := ta.Authenticate(req) assert.FalseT(t, ok) assert.Nil(t, usr) require.NoError(t, err) assert.Equal(t, wisdom, req.Context().Value(original)) assert.Nil(t, req.Context().Value(reason)) assert.Nil(t, req.Context().Value(extra)) }) }) } go-openapi-runtime-decad8f/security/authenticator.go000066400000000000000000000231701520232310000231300ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package security import ( "context" "net/http" "strings" "github.com/go-openapi/errors" "github.com/go-openapi/runtime" ) const ( query = "query" header = "header" accessTokenParam = "access_token" ) // HTTPAuthenticator is a function that authenticates a HTTP request. func HTTPAuthenticator(handler func(*http.Request) (bool, any, error)) runtime.Authenticator { return runtime.AuthenticatorFunc(func(params any) (bool, any, error) { if request, ok := params.(*http.Request); ok { return handler(request) } if scoped, ok := params.(*ScopedAuthRequest); ok { return handler(scoped.Request) } return false, nil, nil }) } // HttpAuthenticator aliases [HTTPAuthenticator] for backward-compatibility. // // Deprecated: use [HTTPAuthenticator] instead. func HttpAuthenticator(handler func(*http.Request) (bool, any, error)) runtime.Authenticator { //nolint:revive return HTTPAuthenticator(handler) } // ScopedAuthenticator is a function that authenticates an [http.Request] against a list of valid scopes. func ScopedAuthenticator(handler func(*ScopedAuthRequest) (bool, any, error)) runtime.Authenticator { return runtime.AuthenticatorFunc(func(params any) (bool, any, error) { if request, ok := params.(*ScopedAuthRequest); ok { return handler(request) } return false, nil, nil }) } // UserPassAuthentication validates a basic-auth credential. // // Implementations comparing the password (or any derived secret) against a // known value MUST use [crypto/subtle.ConstantTimeCompare]: the runtime // extracts the credential from the request and delegates the comparison // here, and does not enforce a constant-time posture on the caller's behalf. type UserPassAuthentication func(string, string) (any, error) // UserPassAuthenticationCtx is the [context.Context]-aware variant of // [UserPassAuthentication]. The same constant-time-comparison guidance // applies. type UserPassAuthenticationCtx func(context.Context, string, string) (context.Context, any, error) // TokenAuthentication validates an API-key token. // // Implementations comparing the token against a known value MUST use // [crypto/subtle.ConstantTimeCompare]; the runtime delegates the comparison // here and does not enforce a constant-time posture on the caller's behalf. type TokenAuthentication func(string) (any, error) // TokenAuthenticationCtx is the [context.Context]-aware variant of // [TokenAuthentication]. The same constant-time-comparison guidance // applies. type TokenAuthenticationCtx func(context.Context, string) (context.Context, any, error) // ScopedTokenAuthentication validates a bearer/OAuth2 token along with the // scopes required for the operation. // // Implementations comparing the token against a known value MUST use // [crypto/subtle.ConstantTimeCompare]; the runtime delegates the comparison // here and does not enforce a constant-time posture on the caller's behalf. type ScopedTokenAuthentication func(string, []string) (any, error) // ScopedTokenAuthenticationCtx is the [context.Context]-aware variant of // [ScopedTokenAuthentication]. The same constant-time-comparison guidance // applies. type ScopedTokenAuthenticationCtx func(context.Context, string, []string) (context.Context, any, error) var DefaultRealmName = "API" type secCtxKey uint8 const ( failedBasicAuth secCtxKey = iota oauth2SchemeName ) func FailedBasicAuth(r *http.Request) string { return FailedBasicAuthCtx(r.Context()) } func FailedBasicAuthCtx(ctx context.Context) string { v, ok := ctx.Value(failedBasicAuth).(string) if !ok { return "" } return v } func OAuth2SchemeName(r *http.Request) string { return OAuth2SchemeNameCtx(r.Context()) } func OAuth2SchemeNameCtx(ctx context.Context) string { v, ok := ctx.Value(oauth2SchemeName).(string) if !ok { return "" } return v } // BasicAuth creates a basic auth authenticator with the provided authentication function. func BasicAuth(authenticate UserPassAuthentication) runtime.Authenticator { return BasicAuthRealm(DefaultRealmName, authenticate) } // BasicAuthRealm creates a basic auth authenticator with the provided authentication function and realm name. func BasicAuthRealm(realm string, authenticate UserPassAuthentication) runtime.Authenticator { if realm == "" { realm = DefaultRealmName } return HttpAuthenticator(func(r *http.Request) (bool, any, error) { if usr, pass, ok := r.BasicAuth(); ok { p, err := authenticate(usr, pass) if err != nil { *r = *r.WithContext(context.WithValue(r.Context(), failedBasicAuth, realm)) } return true, p, err } *r = *r.WithContext(context.WithValue(r.Context(), failedBasicAuth, realm)) return false, nil, nil }) } // BasicAuthCtx creates a basic auth authenticator with the provided authentication function with support for [context.Context]. func BasicAuthCtx(authenticate UserPassAuthenticationCtx) runtime.Authenticator { return BasicAuthRealmCtx(DefaultRealmName, authenticate) } // BasicAuthRealmCtx creates a basic auth authenticator with the provided authentication function and realm name with support for [context.Context]. func BasicAuthRealmCtx(realm string, authenticate UserPassAuthenticationCtx) runtime.Authenticator { if realm == "" { realm = DefaultRealmName } return HttpAuthenticator(func(r *http.Request) (bool, any, error) { if usr, pass, ok := r.BasicAuth(); ok { ctx, p, err := authenticate(r.Context(), usr, pass) if err != nil { ctx = context.WithValue(ctx, failedBasicAuth, realm) } *r = *r.WithContext(ctx) return true, p, err } *r = *r.WithContext(context.WithValue(r.Context(), failedBasicAuth, realm)) return false, nil, nil }) } // APIKeyAuth creates an authenticator that uses a token for authorization. // This token can be obtained from either a header or a query string. func APIKeyAuth(name, in string, authenticate TokenAuthentication) runtime.Authenticator { inl := strings.ToLower(in) if inl != query && inl != header { // panic because this is most likely a typo panic(errors.New(http.StatusInternalServerError, "api key auth: in value needs to be either \"query\" or \"header\"")) } var getToken func(*http.Request) string switch inl { case header: getToken = func(r *http.Request) string { return r.Header.Get(name) } case query: getToken = func(r *http.Request) string { return r.URL.Query().Get(name) } } return HttpAuthenticator(func(r *http.Request) (bool, any, error) { token := getToken(r) if token == "" { return false, nil, nil } p, err := authenticate(token) return true, p, err }) } // APIKeyAuthCtx creates an authenticator that uses a token for authorization with support for [context.Context]. // This token can be obtained from either a header or a query string. func APIKeyAuthCtx(name, in string, authenticate TokenAuthenticationCtx) runtime.Authenticator { inl := strings.ToLower(in) if inl != query && inl != header { // panic because this is most likely a typo panic(errors.New(http.StatusInternalServerError, "api key auth: in value needs to be either \"query\" or \"header\"")) } var getToken func(*http.Request) string switch inl { case header: getToken = func(r *http.Request) string { return r.Header.Get(name) } case query: getToken = func(r *http.Request) string { return r.URL.Query().Get(name) } } return HttpAuthenticator(func(r *http.Request) (bool, any, error) { token := getToken(r) if token == "" { return false, nil, nil } ctx, p, err := authenticate(r.Context(), token) *r = *r.WithContext(ctx) return true, p, err }) } // ScopedAuthRequest contains both the [http.Request] and the required scopes for a particular operation. type ScopedAuthRequest struct { Request *http.Request RequiredScopes []string } // BearerAuth for use with oauth2 flows. func BearerAuth(name string, authenticate ScopedTokenAuthentication) runtime.Authenticator { const prefix = "Bearer " return ScopedAuthenticator(func(r *ScopedAuthRequest) (bool, any, error) { var token string hdr := r.Request.Header.Get(runtime.HeaderAuthorization) if after, ok := strings.CutPrefix(hdr, prefix); ok { token = after } if token == "" { qs := r.Request.URL.Query() token = qs.Get(accessTokenParam) } //#nosec ct, _, _ := runtime.ContentType(r.Request.Header) if token == "" && (ct == "application/x-www-form-urlencoded" || ct == "multipart/form-data") { token = r.Request.FormValue(accessTokenParam) } if token == "" { return false, nil, nil } rctx := context.WithValue(r.Request.Context(), oauth2SchemeName, name) *r.Request = *r.Request.WithContext(rctx) p, err := authenticate(token, r.RequiredScopes) return true, p, err }) } // BearerAuthCtx for use with oauth2 flows with support for [context.Context]. func BearerAuthCtx(name string, authenticate ScopedTokenAuthenticationCtx) runtime.Authenticator { const prefix = "Bearer " return ScopedAuthenticator(func(r *ScopedAuthRequest) (bool, any, error) { var token string hdr := r.Request.Header.Get(runtime.HeaderAuthorization) if after, ok := strings.CutPrefix(hdr, prefix); ok { token = after } if token == "" { qs := r.Request.URL.Query() token = qs.Get(accessTokenParam) } //#nosec ct, _, _ := runtime.ContentType(r.Request.Header) if token == "" && (ct == "application/x-www-form-urlencoded" || ct == "multipart/form-data") { token = r.Request.FormValue(accessTokenParam) } if token == "" { return false, nil, nil } rctx := context.WithValue(r.Request.Context(), oauth2SchemeName, name) ctx, p, err := authenticate(rctx, token, r.RequiredScopes) *r.Request = *r.Request.WithContext(ctx) return true, p, err }) } go-openapi-runtime-decad8f/security/authorizer.go000066400000000000000000000006661520232310000224570ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package security import ( "net/http" "github.com/go-openapi/runtime" ) // Authorized provides a default implementation of the [Authorizer] interface where all // requests are authorized (successful). func Authorized() runtime.Authorizer { return runtime.AuthorizerFunc(func(_ *http.Request, _ any) error { return nil }) } go-openapi-runtime-decad8f/security/authorizer_test.go000066400000000000000000000040001520232310000235000ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package security import ( "context" "net/http" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestAuthorized(t *testing.T) { authorizer := Authorized() err := authorizer.Authorize(nil, nil) require.NoError(t, err) } func TestAuthenticator(t *testing.T) { r, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) require.NoError(t, err) t.Run("with HTTPAuthenticator", func(t *testing.T) { auth := HTTPAuthenticator(func(_ *http.Request) (bool, any, error) { return true, "test", nil }) t.Run("authenticator should work on *http.Request", func(t *testing.T) { isAuth, user, err := auth.Authenticate(r) require.NoError(t, err) assert.TrueT(t, isAuth) assert.Equal(t, "test", user) }) t.Run("authenticator should work on *ScopedAuthRequest", func(t *testing.T) { scoped := &ScopedAuthRequest{Request: r} isAuth, user, err := auth.Authenticate(scoped) require.NoError(t, err) assert.TrueT(t, isAuth) assert.Equal(t, "test", user) }) t.Run("authenticator should return false on other inputs", func(t *testing.T) { isAuth, user, err := auth.Authenticate("") require.NoError(t, err) assert.FalseT(t, isAuth) assert.Empty(t, user) }) }) t.Run("with ScopedAuthenticator", func(t *testing.T) { auth := ScopedAuthenticator(func(_ *ScopedAuthRequest) (bool, any, error) { return true, "test", nil }) t.Run("authenticator should work on *ScopedAuthRequest", func(t *testing.T) { scoped := &ScopedAuthRequest{Request: r} isAuth, user, err := auth.Authenticate(scoped) require.NoError(t, err) assert.TrueT(t, isAuth) assert.Equal(t, "test", user) }) t.Run("authenticator should return false on other inputs", func(t *testing.T) { isAuth, user, err := auth.Authenticate("") require.NoError(t, err) assert.FalseT(t, isAuth) assert.Empty(t, user) }) }) } go-openapi-runtime-decad8f/security/basic_auth_test.go000066400000000000000000000137641520232310000234270ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package security import ( "context" "net/http" "testing" "github.com/go-openapi/errors" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) type secTestKey uint8 const ( original secTestKey = iota extra reason ) const ( wisdom = "The man who is swimming against the stream knows the strength of it." extraWisdom = "Our greatest glory is not in never falling, but in rising every time we fall." expReason = "I like the dreams of the future better than the history of the past." testPassword = "123456" ) func TestBasicAuth(t *testing.T) { basicAuthHandler := UserPassAuthentication(func(user, pass string) (any, error) { if user == principal && pass == testPassword { return principal, nil } return "", errors.Unauthenticated("basic") }) ba := BasicAuth(basicAuthHandler) t.Run("with valid basic auth", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, authPath, nil) require.NoError(t, err) req.SetBasicAuth(principal, testPassword) ok, usr, err := ba.Authenticate(req) require.NoError(t, err) assert.TrueT(t, ok) assert.Equal(t, principal, usr) }) t.Run("with invalid basic auth", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, authPath, nil) require.NoError(t, err) req.SetBasicAuth(principal, principal) ok, usr, err := ba.Authenticate(req) require.Error(t, err) assert.TrueT(t, ok) assert.Empty(t, usr) assert.NotEmpty(t, FailedBasicAuth(req)) assert.EqualT(t, DefaultRealmName, FailedBasicAuth(req)) }) t.Run("with missing basic auth", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, authPath, nil) require.NoError(t, err) ok, usr, err := ba.Authenticate(req) require.NoError(t, err) assert.FalseT(t, ok) assert.Nil(t, usr) assert.NotEmpty(t, FailedBasicAuth(req)) assert.EqualT(t, DefaultRealmName, FailedBasicAuth(req)) }) t.Run("basic auth without request", func(*testing.T) { ok, usr, err := ba.Authenticate("token") require.NoError(t, err) assert.FalseT(t, ok) assert.Nil(t, usr) }) t.Run("with realm, invalid basic auth", func(t *testing.T) { br := BasicAuthRealm("realm", basicAuthHandler) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, authPath, nil) require.NoError(t, err) req.SetBasicAuth(principal, principal) ok, usr, err := br.Authenticate(req) require.Error(t, err) assert.TrueT(t, ok) assert.Empty(t, usr) assert.EqualT(t, "realm", FailedBasicAuth(req)) }) t.Run("with empty realm, invalid basic auth", func(t *testing.T) { br := BasicAuthRealm("", basicAuthHandler) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, authPath, nil) require.NoError(t, err) req.SetBasicAuth(principal, principal) ok, usr, err := br.Authenticate(req) require.Error(t, err) assert.TrueT(t, ok) assert.Empty(t, usr) assert.EqualT(t, DefaultRealmName, FailedBasicAuth(req)) }) } func TestBasicAuthCtx(t *testing.T) { basicAuthHandlerCtx := UserPassAuthenticationCtx(func(ctx context.Context, user, pass string) (context.Context, any, error) { if user == principal && pass == testPassword { return context.WithValue(ctx, extra, extraWisdom), principal, nil } return context.WithValue(ctx, reason, expReason), "", errors.Unauthenticated("basic") }) ba := BasicAuthCtx(basicAuthHandlerCtx) ctx := context.WithValue(context.Background(), original, wisdom) t.Run("with valid basic auth", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, authPath, nil) require.NoError(t, err) req.SetBasicAuth(principal, testPassword) ok, usr, err := ba.Authenticate(req) require.NoError(t, err) assert.TrueT(t, ok) assert.Equal(t, principal, usr) assert.Equal(t, wisdom, req.Context().Value(original)) assert.Equal(t, extraWisdom, req.Context().Value(extra)) assert.Nil(t, req.Context().Value(reason)) }) t.Run("with invalid basic auth", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, authPath, nil) require.NoError(t, err) req.SetBasicAuth(principal, principal) ok, usr, err := ba.Authenticate(req) require.Error(t, err) assert.TrueT(t, ok) assert.Empty(t, usr) assert.Equal(t, wisdom, req.Context().Value(original)) assert.Nil(t, req.Context().Value(extra)) assert.Equal(t, expReason, req.Context().Value(reason)) }) t.Run("with missing basic auth", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, authPath, nil) require.NoError(t, err) ok, usr, err := ba.Authenticate(req) require.NoError(t, err) assert.FalseT(t, ok) assert.Nil(t, usr) assert.Equal(t, wisdom, req.Context().Value(original)) assert.Nil(t, req.Context().Value(extra)) assert.Nil(t, req.Context().Value(reason)) }) t.Run("basic auth without request", func(*testing.T) { ok, usr, err := ba.Authenticate("token") require.NoError(t, err) assert.FalseT(t, ok) assert.Nil(t, usr) }) t.Run("with realm, invalid basic auth", func(t *testing.T) { br := BasicAuthRealmCtx("realm", basicAuthHandlerCtx) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, authPath, nil) require.NoError(t, err) req.SetBasicAuth(principal, principal) ok, usr, err := br.Authenticate(req) require.Error(t, err) assert.TrueT(t, ok) assert.Empty(t, usr) assert.EqualT(t, "realm", FailedBasicAuth(req)) }) t.Run("with empty realm, invalid basic auth", func(t *testing.T) { br := BasicAuthRealmCtx("", basicAuthHandlerCtx) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, authPath, nil) require.NoError(t, err) req.SetBasicAuth(principal, principal) ok, usr, err := br.Authenticate(req) require.Error(t, err) assert.TrueT(t, ok) assert.Empty(t, usr) assert.EqualT(t, DefaultRealmName, FailedBasicAuth(req)) }) } go-openapi-runtime-decad8f/security/bearer_auth_test.go000066400000000000000000000241311520232310000235740ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package security import ( "bytes" "context" "fmt" "mime/multipart" "net/http" "net/url" "strings" "testing" "github.com/go-openapi/errors" "github.com/go-openapi/runtime" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) const ( owners = "owners_auth" validToken = "token123" invalidToken = "token124" principal = "admin" authPath = "/blah" invalidParam = "access_toke" ) type authExpectation uint8 const ( expectIsAuthorized authExpectation = iota expectInvalidAuthorization expectNoAuthorization ) func TestBearerAuth(t *testing.T) { bearerAuth := ScopedTokenAuthentication(func(token string, _ []string) (any, error) { if token == validToken { return principal, nil } return nil, errors.Unauthenticated("bearer") }) ba := BearerAuth(owners, bearerAuth) ctx := context.Background() t.Run("with valid bearer auth", func(t *testing.T) { t.Run("token in query param", testAuthenticateBearerInQuery(ctx, ba, "", validToken, expectIsAuthorized), ) t.Run("token in header", testAuthenticateBearerInHeader(ctx, ba, "", validToken, expectIsAuthorized), ) t.Run("token in urlencoded form", testAuthenticateBearerInForm(ctx, ba, "", validToken, expectIsAuthorized), ) t.Run("token in multipart form", testAuthenticateBearerInMultipartForm(ctx, ba, "", validToken, expectIsAuthorized), ) }) t.Run("with invalid token", func(t *testing.T) { t.Run("token in query param", testAuthenticateBearerInQuery(ctx, ba, "", invalidToken, expectInvalidAuthorization), ) t.Run("token in header", testAuthenticateBearerInHeader(ctx, ba, "", invalidToken, expectInvalidAuthorization), ) t.Run("token in urlencoded form", testAuthenticateBearerInForm(ctx, ba, "", invalidToken, expectInvalidAuthorization), ) t.Run("token in multipart form", testAuthenticateBearerInMultipartForm(ctx, ba, "", invalidToken, expectInvalidAuthorization), ) }) t.Run("with missing auth", func(t *testing.T) { t.Run("token in query param", testAuthenticateBearerInQuery(ctx, ba, invalidParam, validToken, expectNoAuthorization), ) t.Run("token in header", testAuthenticateBearerInHeader(ctx, ba, "Beare", validToken, expectNoAuthorization), ) t.Run("token in urlencoded form", testAuthenticateBearerInForm(ctx, ba, invalidParam, validToken, expectNoAuthorization), ) t.Run("token in multipart form", testAuthenticateBearerInMultipartForm(ctx, ba, invalidParam, validToken, expectNoAuthorization), ) }) } func TestBearerAuthCtx(t *testing.T) { bearerAuthCtx := ScopedTokenAuthenticationCtx(func(ctx context.Context, token string, _ []string) (context.Context, any, error) { if token == validToken { return context.WithValue(ctx, extra, extraWisdom), principal, nil } return context.WithValue(ctx, reason, expReason), nil, errors.Unauthenticated("bearer") }) ba := BearerAuthCtx(owners, bearerAuthCtx) ctx := context.WithValue(context.Background(), original, wisdom) assertContextOK := func(requestContext context.Context, t *testing.T) { // when authorized, we have an "extra" key in context assert.Equal(t, wisdom, requestContext.Value(original)) assert.Equal(t, extraWisdom, requestContext.Value(extra)) assert.Nil(t, requestContext.Value(reason)) } assertContextKO := func(requestContext context.Context, t *testing.T) { // when not authorized, we have a "reason" key in context assert.Equal(t, wisdom, requestContext.Value(original)) assert.Nil(t, requestContext.Value(extra)) assert.Equal(t, expReason, requestContext.Value(reason)) } assertContextNone := func(requestContext context.Context, t *testing.T) { // when missing authorization, we only have the original context assert.Equal(t, wisdom, requestContext.Value(original)) assert.Nil(t, requestContext.Value(extra)) assert.Nil(t, requestContext.Value(reason)) } t.Run("with valid bearer auth", func(t *testing.T) { t.Run("token in query param", testAuthenticateBearerInQuery(ctx, ba, "", validToken, expectIsAuthorized, assertContextOK), ) t.Run("token in header", testAuthenticateBearerInHeader(ctx, ba, "", validToken, expectIsAuthorized, assertContextOK), ) t.Run("token in urlencoded form", testAuthenticateBearerInForm(ctx, ba, "", validToken, expectIsAuthorized, assertContextOK), ) t.Run("token in multipart form", testAuthenticateBearerInMultipartForm(ctx, ba, "", validToken, expectIsAuthorized, assertContextOK), ) }) t.Run("with invalid token", func(t *testing.T) { t.Run("token in query param", testAuthenticateBearerInQuery(ctx, ba, "", invalidToken, expectInvalidAuthorization, assertContextKO), ) t.Run("token in header", testAuthenticateBearerInHeader(ctx, ba, "", invalidToken, expectInvalidAuthorization, assertContextKO), ) t.Run("token in urlencoded form", testAuthenticateBearerInForm(ctx, ba, "", invalidToken, expectInvalidAuthorization, assertContextKO), ) t.Run("token in multipart form", testAuthenticateBearerInMultipartForm(ctx, ba, "", invalidToken, expectInvalidAuthorization, assertContextKO), ) }) t.Run("with missing auth", func(t *testing.T) { t.Run("token in query param", testAuthenticateBearerInQuery(ctx, ba, invalidParam, validToken, expectNoAuthorization, assertContextNone), ) t.Run("token in header", testAuthenticateBearerInHeader(ctx, ba, "Beare", validToken, expectNoAuthorization, assertContextNone), ) t.Run("token in urlencoded form", testAuthenticateBearerInForm(ctx, ba, invalidParam, validToken, expectNoAuthorization, assertContextNone), ) t.Run("token in multipart form", testAuthenticateBearerInMultipartForm(ctx, ba, invalidParam, validToken, expectNoAuthorization, assertContextNone), ) }) } func testIsAuthorized(_ context.Context, req *http.Request, authorizer runtime.Authenticator, expectAuthorized authExpectation, extraAsserters ...func(context.Context, *testing.T), ) func(*testing.T) { return func(t *testing.T) { //nolint:contextcheck hasToken, usr, err := authorizer.Authenticate(&ScopedAuthRequest{Request: req}) switch expectAuthorized { case expectIsAuthorized: require.NoError(t, err) assert.TrueT(t, hasToken) assert.Equal(t, principal, usr) assert.EqualT(t, owners, OAuth2SchemeName(req)) case expectInvalidAuthorization: require.Error(t, err) require.ErrorContains(t, err, "unauthenticated") assert.TrueT(t, hasToken) assert.Nil(t, usr) assert.EqualT(t, owners, OAuth2SchemeName(req)) case expectNoAuthorization: require.NoError(t, err) assert.FalseT(t, hasToken) assert.Nil(t, usr) assert.Empty(t, OAuth2SchemeName(req)) default: t.FailNow() } for _, contextAsserter := range extraAsserters { contextAsserter(req.Context(), t) } } } func shouldAuthorizeOrNot(expectAuthorized authExpectation) string { if expectAuthorized == expectIsAuthorized { return "should authorize" } return "should not authorize" } func testAuthenticateBearerInQuery( // build a request with the token as a query parameter, then check against the authorizer // // the request context after authorization may be checked with the extraAsserters. ctx context.Context, authorizer runtime.Authenticator, parameter, token string, expectAuthorized authExpectation, extraAsserters ...func(context.Context, *testing.T), ) func(*testing.T) { if parameter == "" { parameter = accessTokenParam } return func(t *testing.T) { req, err := http.NewRequestWithContext( ctx, http.MethodGet, fmt.Sprintf("%s?%s=%s", authPath, parameter, token), nil, ) require.NoError(t, err) t.Run( shouldAuthorizeOrNot(expectAuthorized), testIsAuthorized(ctx, req, authorizer, expectAuthorized, extraAsserters...), ) } } func testAuthenticateBearerInHeader( // build a request with the token as a header, then check against the authorizer ctx context.Context, authorizer runtime.Authenticator, parameter, token string, expectAuthorized authExpectation, extraAsserters ...func(context.Context, *testing.T), ) func(*testing.T) { if parameter == "" { parameter = "Bearer" } return func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, authPath, nil) require.NoError(t, err) req.Header.Set(runtime.HeaderAuthorization, fmt.Sprintf("%s %s", parameter, token)) t.Run( shouldAuthorizeOrNot(expectAuthorized), testIsAuthorized(ctx, req, authorizer, expectAuthorized, extraAsserters...), ) } } func testAuthenticateBearerInForm( // build a request with the token as a form field, then check against the authorizer ctx context.Context, authorizer runtime.Authenticator, parameter, token string, expectAuthorized authExpectation, extraAsserters ...func(context.Context, *testing.T), ) func(*testing.T) { if parameter == "" { parameter = accessTokenParam } return func(t *testing.T) { body := url.Values(map[string][]string{}) body.Set(parameter, token) req, err := http.NewRequestWithContext(ctx, http.MethodPost, authPath, strings.NewReader(body.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") t.Run( shouldAuthorizeOrNot(expectAuthorized), testIsAuthorized(ctx, req, authorizer, expectAuthorized, extraAsserters...), ) } } func testAuthenticateBearerInMultipartForm( // build a request with the token as a multipart form field, then check against the authorizer ctx context.Context, authorizer runtime.Authenticator, parameter, token string, expectAuthorized authExpectation, extraAsserters ...func(context.Context, *testing.T), ) func(*testing.T) { if parameter == "" { parameter = accessTokenParam } return func(t *testing.T) { body := bytes.NewBuffer(nil) writer := multipart.NewWriter(body) require.NoError(t, writer.WriteField(parameter, token)) require.NoError(t, writer.Close()) req, err := http.NewRequestWithContext(ctx, http.MethodPost, authPath, body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) t.Run( shouldAuthorizeOrNot(expectAuthorized), testIsAuthorized(ctx, req, authorizer, expectAuthorized, extraAsserters...), ) } } go-openapi-runtime-decad8f/server-middleware/000077500000000000000000000000001520232310000214765ustar00rootroot00000000000000go-openapi-runtime-decad8f/server-middleware/README.md000066400000000000000000000047671520232310000227730ustar00rootroot00000000000000# server-middleware [![GoDoc][godoc-badge]][godoc-url] Standalone, dependency-free server-side middleware utilities for OpenAPI applications. This module is part of the [`go-openapi/runtime`][runtime-url] toolkit, but is maintained as a **separate Go module** so it can be used by any `net/http` application — including ones that have no OpenAPI spec at all. It carries no transitive dependency on `go-openapi/spec`, `go-openapi/loads`, or `go-openapi/validate`; only the standard library and (for tests) `go-openapi/testify/v2`. ## Packages | Package | Purpose | |---------|---------| | [`mediatype`](./mediatype) | Typed RFC 7231 / RFC 2045 media-type values (`MediaType`, `Set`) and asymmetric matching used by both server-side validation and `Accept`-header negotiation. | | [`negotiate`](./negotiate) | Server-side HTTP content negotiation: select the response `Content-Type` from `Accept`, and the response `Content-Encoding` from `Accept-Encoding`. Honours MIME parameters by default; opt out with `WithIgnoreParameters`. | | [`negotiate/header`](./negotiate/header) | Low-level RFC-7231 header parsing primitives reused by `negotiate`. Exported for callers that need raw `Accept`/`Accept-Encoding` parsing without the typed media-type layer. | | [`docui`](./docui) | Stdlib-only HTTP middlewares that serve OpenAPI documentation UIs (Swagger UI, ReDoc, RapiDoc) and the spec document itself. Mountable on any `net/http` mux. | ## Install ```sh go get github.com/go-openapi/runtime/server-middleware ``` ## Quick start — content negotiation ```go import "github.com/go-openapi/runtime/server-middleware/negotiate" offers := []string{"application/json", "application/xml"} chosen := negotiate.ContentType(r.Header, offers, "application/json") w.Header().Set("Content-Type", chosen) ``` ## Quick start — serving Swagger UI ```go import "github.com/go-openapi/runtime/server-middleware/docui" handler := docui.SwaggerUI(docui.WithSpecURL("/swagger.json"), docui.WithBasePath("/docs")) http.Handle("/docs/", handler) ``` ## Further reading - [media-type selection tutorial](https://go-openapi.github.io/runtime/tutorials/media-types/) — the full server-side selection algorithm and its asymmetric matching rules. - Full API reference on [pkg.go.dev][godoc-url]. ## License [Apache-2.0](../LICENSE). [runtime-url]: https://github.com/go-openapi/runtime [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/runtime/server-middleware [godoc-url]: https://pkg.go.dev/github.com/go-openapi/runtime/server-middleware go-openapi-runtime-decad8f/server-middleware/doc.go000066400000000000000000000002711520232310000225720ustar00rootroot00000000000000// Package middleware exposes middleware utilities for OpenAPI. // // This is a standalone module, with minimal dependencies to the rest of the go-openapi libraries. package middleware go-openapi-runtime-decad8f/server-middleware/docui/000077500000000000000000000000001520232310000226015ustar00rootroot00000000000000go-openapi-runtime-decad8f/server-middleware/docui/doc.go000066400000000000000000000007721520232310000237030ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // Package docui provides standalone HTTP middlewares that serve OpenAPI // documentation UIs (Swagger UI, ReDoc, RapiDoc) and the spec document // itself. // // The package is stdlib-only and has no transitive dependency on any // OpenAPI spec, loading or validation library, so it may be imported by // any net/http application that simply wants to mount a documentation // site. package docui go-openapi-runtime-decad8f/server-middleware/docui/options.go000066400000000000000000000143771520232310000246370ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "net/http" "net/url" "strings" ) const ( // constants that are common to all UI-serving middlewares. defaultDocsPath = "docs" defaultDocsURL = "/swagger.json" defaultDocsTitle = "API Documentation" contentTypeHeader = "Content-Type" applicationJSON = "application/json" ) // UIMiddleware is a function returning a http middleware which accepts UI [Option]. type UIMiddleware func(...Option) func(http.Handler) http.Handler // Option to tune your swagger documentation UI middleware. // // Options may be combined to alter the route at which the UI asset is served, // the URL of the spec document, the source URL of the UI asset and the title of the UI page. // // The embedded js scriptlet served may be modified using [WithUITemplate]. type Option func(*options) // SpecOption can be applied to the [ServeSpec] middleware. type SpecOption func(*specOptions) // SwaggerUIOptions define a group of extra options specific to the SwaggerUI component. type SwaggerUIOptions struct { // OAuth2CallbackURL sets the URL called after OAuth2 login OAuth2CallbackURL string // Defines the URL of the swagger UI assets with presets. // // Default: https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js SwaggerPresetURL string // Defines style sheet URL. // // Default: https://unpkg.com/swagger-ui-dist/swagger-ui.css SwaggerStylesURL string // Define the favicons URLs. // // Defaults: // // - 16x16: https://unpkg.com/swagger-ui-dist/favicon-16x16.png // - 32x32: https://unpkg.com/swagger-ui-dist/favicon-32x32.png Favicon32 string Favicon16 string } func (o *SwaggerUIOptions) applySwaggerUIDefaults() { if o.SwaggerPresetURL == "" { o.SwaggerPresetURL = swaggerPresetLatest } if o.SwaggerStylesURL == "" { o.SwaggerStylesURL = swaggerStylesLatest } if o.Favicon16 == "" || o.Favicon32 == "" { o.Favicon16 = swaggerFavicon16Latest o.Favicon32 = swaggerFavicon32Latest } } type ( options struct { SwaggerUIOptions // BasePath for the UI, defaults to: / BasePath string // Path combines with BasePath to construct the path to the UI, defaults to: "docs". Path string // SpecURL is the URL of the spec document. SpecURL string // Title for the documentation site, default to: API documentation Title string // Template specifies a custom template to serve the UI Template string // AssetsURL points to the js asset that generates the documentation page. AssetsURL string } specOptions struct { Path string Document string } ) //////////////////////////////////////////////////////////// // Common UI options //////////////////////////////////////////////////////////// // WithUIBasePath sets the base path from where to serve the UI assets. // // Default: "/" func WithUIBasePath(base string) Option { return func(o *options) { if !strings.HasPrefix(base, "/") { base = "/" + base } o.BasePath = base } } // WithUIPath sets the path from where to serve the UI assets (i.e. /{basepath}/{path}). // // Default: "docs" func WithUIPath(pth string) Option { return func(o *options) { o.Path = pth } } // WithUITitle sets the title of the UI. // // Default: "API documentation" func WithUITitle(title string) Option { return func(o *options) { o.Title = title } } // WithUIAssetsURL sets the URL from where to fetch the js assets. // // Defaults: // // - for Redoc: https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js // - for RapiDoc, this defaults to: https://unpkg.com/rapidoc/dist/rapidoc-min.js // - for SwaggerUI: https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js func WithUIAssetsURL(assets string) Option { return func(o *options) { o.AssetsURL = assets } } // WithUITemplate allows to set a custom template for the UI. // // This allows the caller to fully customize the rendered UI, using the advanced options // provided by any UI. // // The UI [middleware] will panic if the template does not parse or execute properly. // // Reference documentations to customize your js scriptlet: // // - for Redoc: https://github.com/Redocly/redoc/blob/main/docs/deployment/html.md // - for RapiDoc: https://github.com/rapi-doc/RapiDoc // - for SwaggerUI: https://github.com/swagger-api/swagger-ui func WithUITemplate[StringOrBytes ~string | ~[]byte](tpl StringOrBytes) Option { return func(o *options) { o.Template = string(tpl) } } // WithSpecURL sets the URL of the spec document. // // Defaults to: /swagger.json func WithSpecURL(u string) Option { return func(o *options) { o.SpecURL = u } } //////////////////////////////////////////////////////////// // SwaggerUI UI options //////////////////////////////////////////////////////////// func WithSwaggerUIOptions(opts SwaggerUIOptions) Option { return func(o *options) { o.SwaggerUIOptions = opts } } //////////////////////////////////////////////////////////// // Spec options //////////////////////////////////////////////////////////// // WithSpecPath sets the path of the spec document. // // This is "/swagger.json" by default. func WithSpecPath(pth string) SpecOption { return func(o *specOptions) { if pth == "" { return } o.Path = pth } } // WithSpecPathFromOptions reuses the same SpecPath as the one specified in // a set of UI [Option] (extract the path from the URL provided by [WithSpecURL]). func WithSpecPathFromOptions(opts ...Option) SpecOption { return func(o *specOptions) { uiOpts := optionsWithDefaults(opts) // If the spec URL is provided, there is a non-default path to serve the spec. // // This makes sure that the UI middleware is aligned with the Spec middleware. u, _ := url.Parse(uiOpts.SpecURL) if u.Path == "" { return } o.Path = u.Path } } func optionsWithDefaults(opts []Option, prepend ...Option) options { o := options{ BasePath: "/", Path: defaultDocsPath, SpecURL: defaultDocsURL, Title: defaultDocsTitle, } prepend = append(prepend, opts...) for _, apply := range prepend { apply(&o) } return o } func specOptionsWithDefaults(opts []SpecOption) specOptions { o := specOptions{ Path: defaultDocsURL, } for _, apply := range opts { apply(&o) } if !strings.HasPrefix(o.Path, "/") { o.Path = "/" + o.Path } return o } go-openapi-runtime-decad8f/server-middleware/docui/rapidoc.go000066400000000000000000000034011520232310000245470ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "bytes" "fmt" "html/template" "net/http" "path" ) // UseRapiDoc creates a middleware to serve a documentation site for a swagger spec using [RapidDoc]. // // [RapiDoc]: https://github.com/rapi-doc/RapiDoc func UseRapiDoc(opts ...Option) func(next http.Handler) http.Handler { pth, assets := rapiDocSetup(opts) return func(next http.Handler) http.Handler { return serveUI(pth, assets, next) } } // RapiDoc creates a [http.Handler] to serve a documentation site for a swagger spec using [RapidDoc]. // // By default, the UI is served at route "/docs" // // This allows for altering the spec before starting the [http] listener. // // [RapiDoc]: https://github.com/rapi-doc/RapiDoc func RapiDoc(next http.Handler, opts ...Option) http.Handler { pth, assets := rapiDocSetup(opts) return serveUI(pth, assets, next) } func rapiDocSetup(opts []Option) (pth string, assets []byte) { o := optionsWithDefaults(opts, // defaults for rapiDoc WithUITemplate(rapidocTemplate), WithUIAssetsURL(rapidocLatest), ) pth = path.Join(o.BasePath, o.Path) tmpl := template.Must(template.New("rapidoc").Parse(o.Template)) buf := bytes.NewBuffer(nil) if err := tmpl.Execute(buf, o); err != nil { panic(fmt.Errorf("cannot execute template: %w", err)) } return pth, buf.Bytes() } const ( rapidocLatest = "https://unpkg.com/rapidoc/dist/rapidoc-min.js" rapidocTemplate = ` {{ .Title }} ` ) go-openapi-runtime-decad8f/server-middleware/docui/rapidoc_test.go000066400000000000000000000066011520232310000256130ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "context" "fmt" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestRapiDocMiddleware(t *testing.T) { t.Run("with defaults", func(t *testing.T) { h := RapiDoc(nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) assert.EqualT(t, "text/html; charset=utf-8", recorder.Header().Get(contentTypeHeader)) body := recorder.Body.String() assert.StringContainsT(t, body, fmt.Sprintf("%s", defaultDocsTitle)) assert.StringContainsT(t, body, fmt.Sprintf(``, defaultDocsURL)) assert.StringContainsT(t, body, rapidocLatest) }) t.Run("with alternate path and spec URL", func(t *testing.T) { h := RapiDoc(nil, WithUIBasePath("/base"), WithUIPath("ui"), WithSpecURL("/ui/swagger.json"), ) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/base/ui", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) assert.StringContainsT(t, recorder.Body.String(), ``) }) t.Run("with custom assets URL", func(t *testing.T) { h := RapiDoc(nil, WithUIAssetsURL("https://example.com/rapidoc.js")) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) assert.StringContainsT(t, recorder.Body.String(), `src="https://example.com/rapidoc.js"`) }) t.Run("falls through to next handler", func(t *testing.T) { next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusTeapot) }) h := RapiDoc(next) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/elsewhere", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusTeapot, recorder.Code) }) t.Run("returns 404 when no next handler", func(t *testing.T) { h := RapiDoc(nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/elsewhere", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusNotFound, recorder.Code) }) t.Run("edge cases", func(t *testing.T) { t.Run("with template that fails to execute", func(t *testing.T) { assert.Panics(t, func() { RapiDoc(nil, WithUITemplate(badTemplate)) }) }) }) } func TestUseRapiDoc(t *testing.T) { t.Run("composes as a func(http.Handler) http.Handler", func(t *testing.T) { next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusTeapot) }) h := UseRapiDoc()(next) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) }) } go-openapi-runtime-decad8f/server-middleware/docui/redoc.go000066400000000000000000000041451520232310000242300ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "bytes" "fmt" "html/template" "net/http" "path" ) // UseRedoc creates a middleware to serve a documentation site for a swagger spec using [Redoc]. // // [Redoc]: https://redocly.com/docs/redoc func UseRedoc(opts ...Option) func(next http.Handler) http.Handler { pth, assets := redocSetup(opts) return func(next http.Handler) http.Handler { return serveUI(pth, assets, next) } } // Redoc creates a [http.Handler] to serve a documentation site for a swagger spec using [Redoc]. // // By default, the UI is served at route "/docs" // // This allows for altering the spec before starting the [http] listener. // // [Redoc]: https://redocly.com/docs/redoc func Redoc(next http.Handler, opts ...Option) http.Handler { pth, assets := redocSetup(opts) return serveUI(pth, assets, next) } func redocSetup(opts []Option) (pth string, assets []byte) { o := optionsWithDefaults(opts, // defaults for redoc WithUITemplate(redocTemplate), WithUIAssetsURL(redocLatest), ) pth = path.Join(o.BasePath, o.Path) tmpl := template.Must(template.New("redoc").Parse(o.Template)) buf := bytes.NewBuffer(nil) if err := tmpl.Execute(buf, o); err != nil { panic(fmt.Errorf("cannot execute template: %w", err)) } return pth, buf.Bytes() } const ( redocLatest = "https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js" // "https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js" redocTemplate = ` {{ .Title }} ` ) go-openapi-runtime-decad8f/server-middleware/docui/redoc_test.go000066400000000000000000000107051520232310000252660ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "context" "fmt" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestRedocMiddleware(t *testing.T) { t.Run("with defaults", func(t *testing.T) { h := Redoc(nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) assert.EqualT(t, "text/html; charset=utf-8", recorder.Header().Get(contentTypeHeader)) body := recorder.Body.String() assert.StringContainsT(t, body, fmt.Sprintf("%s", defaultDocsTitle)) assert.StringContainsT(t, body, fmt.Sprintf("", defaultDocsURL)) assert.StringContainsT(t, body, redocLatest) }) t.Run("with alternate path and spec URL", func(t *testing.T) { h := Redoc(nil, WithUIBasePath("/base"), WithUIPath("ui"), WithSpecURL("/ui/swagger.json"), ) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/base/ui", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) assert.StringContainsT(t, recorder.Body.String(), "") }) t.Run("with custom assets URL", func(t *testing.T) { h := Redoc(nil, WithUIAssetsURL("https://example.com/redoc.js")) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) assert.StringContainsT(t, recorder.Body.String(), ` ` h := Redoc(nil, WithUITemplate(tpl)) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) assert.StringContainsT(t, recorder.Body.String(), "required-props-first=true") }) t.Run("falls through to next handler", func(t *testing.T) { next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusTeapot) }) h := Redoc(next) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/elsewhere", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusTeapot, recorder.Code) }) t.Run("returns 404 when no next handler", func(t *testing.T) { h := Redoc(nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/elsewhere", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusNotFound, recorder.Code) }) t.Run("edge cases", func(t *testing.T) { t.Run("with malformed template", func(t *testing.T) { assert.Panics(t, func() { Redoc(nil, WithUITemplate(malformedTemplate)) }) }) t.Run("with template that fails to execute", func(t *testing.T) { assert.Panics(t, func() { Redoc(nil, WithUITemplate(badTemplate)) }) }) }) } func TestUseRedoc(t *testing.T) { t.Run("composes as a func(http.Handler) http.Handler", func(t *testing.T) { next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusTeapot) }) h := UseRedoc()(next) t.Run("serves the docs page", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) }) t.Run("forwards everything else", func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/elsewhere", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusTeapot, recorder.Code) }) }) } go-openapi-runtime-decad8f/server-middleware/docui/render.go000066400000000000000000000014031520232310000244050ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "fmt" "net/http" "path" ) // serveUI creates a [http.Handler] that serves a templated asset as text/html. func serveUI(pth string, assets []byte, next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if path.Clean(r.URL.Path) == pth { rw.Header().Set(contentTypeHeader, "text/html; charset=utf-8") rw.WriteHeader(http.StatusOK) _, _ = rw.Write(assets) return } if next != nil { next.ServeHTTP(rw, r) return } rw.Header().Set(contentTypeHeader, "text/plain") rw.WriteHeader(http.StatusNotFound) _, _ = fmt.Fprintf(rw, "%q not found", pth) }) } go-openapi-runtime-decad8f/server-middleware/docui/render_test.go000066400000000000000000000015561520232310000254550ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui // minimal valid swagger 2.0 document, sufficient for round-tripping bytes // without dragging in the petstore fixture (which lives in an internal/ // package of the parent runtime module and is not importable from here). var testSpec = []byte(`{"swagger":"2.0","info":{"title":"Test","version":"1.0.0"},"paths":{}}`) // badTemplate references a field that does not exist on the [options] // struct. Parsing succeeds but execution fails — exercising the // template.Execute panic branch in every UI handler. const badTemplate = ` spec-url='{{ .Unknown }}' ` // malformedTemplate fails at parse time (open action with no close). const malformedTemplate = ` spec-url='{{ .Spec ` go-openapi-runtime-decad8f/server-middleware/docui/spec.go000066400000000000000000000025211520232310000240620ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "net/http" "path" ) // UseSpec creates a middleware to serve a swagger spec as a JSON document. func UseSpec(spec []byte, opts ...SpecOption) func(next http.Handler) http.Handler { o := specOptionsWithDefaults(opts) return func(next http.Handler) http.Handler { return handleSpec(o.Path, spec, next) } } // ServeSpec creates a [http.Handler] to serve a swagger spec as a JSON document. // // This allows for altering the spec before starting the [http] listener. // // Additional [SpecOption] can be used to change the path and the name of the document (defaults to "/swagger.json"). func ServeSpec(spec []byte, next http.Handler, opts ...SpecOption) http.Handler { o := specOptionsWithDefaults(opts) return handleSpec(o.Path, spec, next) } func handleSpec(pth string, spec []byte, next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if path.Clean(r.URL.Path) == pth { rw.Header().Set(contentTypeHeader, applicationJSON) rw.WriteHeader(http.StatusOK) _, _ = rw.Write(spec) return } if next != nil { next.ServeHTTP(rw, r) return } rw.Header().Set(contentTypeHeader, applicationJSON) rw.WriteHeader(http.StatusNotFound) }) } go-openapi-runtime-decad8f/server-middleware/docui/spec_test.go000066400000000000000000000050511520232310000251220ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "context" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestServeSpecMiddleware(t *testing.T) { t.Run("ServeSpec handler", func(t *testing.T) { handler := ServeSpec(testSpec, nil) t.Run("serves spec", func(t *testing.T) { request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/swagger.json", nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) responseHeaders := recorder.Result().Header responseContentType := responseHeaders.Get(contentTypeHeader) assert.EqualT(t, applicationJSON, responseContentType) require.JSONEqT(t, string(testSpec), recorder.Body.String()) }) t.Run("returns 404 when no next handler", func(t *testing.T) { request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusNotFound, recorder.Code) }) t.Run("forwards to next handler for other url", func(t *testing.T) { handler = ServeSpec(testSpec, http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusOK) })) request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/api/pets", nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) }) }) t.Run("ServeSpec handler with options", func(t *testing.T) { handler := ServeSpec(testSpec, nil, WithSpecPath("/swagger/spec/myapi-swagger.json"), ) t.Run("serves spec", func(t *testing.T) { request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/swagger/spec/myapi-swagger.json", nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusOK, recorder.Code) }) t.Run("should not find spec there", func(t *testing.T) { request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/swagger.json", nil) require.NoError(t, err) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) assert.EqualT(t, http.StatusNotFound, recorder.Code) }) }) } go-openapi-runtime-decad8f/server-middleware/docui/swaggerui.go000066400000000000000000000070751520232310000251360ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "bytes" "fmt" "html/template" "net/http" "path" ) // UseSwaggerUI creates a middleware to serve a documentation site for a swagger spec using [SwaggerUI]. // // [SwaggerUI]: https://swagger.io/tools/swagger-ui func UseSwaggerUI(opts ...Option) func(next http.Handler) http.Handler { pth, assets := swaggeruiSetup(opts) return func(next http.Handler) http.Handler { return serveUI(pth, assets, next) } } // SwaggerUI creates a [http.Handler] to serve a documentation site for a swagger spec using [SwaggerUI]. // // By default, the UI is served at route "/docs" // // This allows for altering the spec before starting the [http] listener. // // [SwaggerUI]: https://swagger.io/tools/swagger-ui func SwaggerUI(next http.Handler, opts ...Option) http.Handler { pth, assets := swaggeruiSetup(opts) return serveUI(pth, assets, next) } func swaggeruiSetup(opts []Option) (pth string, assets []byte) { o := optionsWithDefaults(opts, // defaults for SwaggerUI WithUITemplate(swaggeruiTemplate), WithUIAssetsURL(swaggerLatest), ) o.applySwaggerUIDefaults() if o.OAuth2CallbackURL == "" { o.OAuth2CallbackURL = path.Join(o.BasePath, o.Path, "oauth2-callback") } pth = path.Join(o.BasePath, o.Path) tmpl := template.Must(template.New("swaggerui").Parse(o.Template)) buf := bytes.NewBuffer(nil) if err := tmpl.Execute(buf, o); err != nil { panic(fmt.Errorf("cannot execute template: %w", err)) } return pth, buf.Bytes() } const ( swaggerLatest = "https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js" swaggerPresetLatest = "https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js" swaggerStylesLatest = "https://unpkg.com/swagger-ui-dist/swagger-ui.css" swaggerFavicon32Latest = "https://unpkg.com/swagger-ui-dist/favicon-32x32.png" swaggerFavicon16Latest = "https://unpkg.com/swagger-ui-dist/favicon-16x16.png" swaggeruiTemplate = ` {{ .Title }} {{- if .SwaggerStylesURL }} {{- end }} {{- if .Favicon32 }} {{- end }} {{- if .Favicon16 }} {{- end }}
{{- if .SwaggerPresetURL }} {{- end }} ` ) go-openapi-runtime-decad8f/server-middleware/docui/swaggerui_oauth2.go000066400000000000000000000101161520232310000264060ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "bytes" "fmt" "net/http" "path" "text/template" ) // UseSwaggerUIOAuth2Callback creates a middleware that serves a callback URL to complete // a OAuth2 token handshake. func UseSwaggerUIOAuth2Callback(opts ...Option) func(next http.Handler) http.Handler { pth, assets := swaggeruiOAuth2Setup(opts) return func(next http.Handler) http.Handler { return serveUI(pth, assets, next) } } // SwaggerUIOAuth2Callback creates a [http.Handler] that serves a callback URL to complete // a OAuth2 token handshake. func SwaggerUIOAuth2Callback(next http.Handler, opts ...Option) http.Handler { pth, assets := swaggeruiOAuth2Setup(opts) return serveUI(pth, assets, next) } func swaggeruiOAuth2Setup(opts []Option) (pth string, assets []byte) { o := optionsWithDefaults(opts, // defaults for SwaggerUI OAuth2 callback endpoint WithUITemplate(swaggerOAuth2Template), WithUIAssetsURL(swaggerLatest), ) o.applySwaggerUIDefaults() if o.OAuth2CallbackURL == "" { o.OAuth2CallbackURL = path.Join(o.BasePath, o.Path, "oauth2-callback") } pth = o.OAuth2CallbackURL tmpl := template.Must(template.New("swaggeroauth2").Parse(o.Template)) buf := bytes.NewBuffer(nil) if err := tmpl.Execute(buf, o); err != nil { panic(fmt.Errorf("cannot execute template: %w", err)) } return pth, buf.Bytes() } const swaggerOAuth2Template = ` {{ .Title }} ` go-openapi-runtime-decad8f/server-middleware/docui/swaggerui_oauth2_test.go000066400000000000000000000067661520232310000274650ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "context" "fmt" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestSwaggerUIOAuth2CallbackMiddleware(t *testing.T) { t.Run("with defaults", func(t *testing.T) { h := SwaggerUIOAuth2Callback(nil) // Default callback URL is ///oauth2-callback, // i.e. /docs/oauth2-callback. req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs/oauth2-callback", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) require.EqualT(t, http.StatusOK, recorder.Code) assert.EqualT(t, "text/html; charset=utf-8", recorder.Header().Get(contentTypeHeader)) body := recorder.Body.String() assert.StringContainsT(t, body, fmt.Sprintf("%s", defaultDocsTitle)) // Marker from the swagger-ui-dist OAuth2 popup callback script. assert.StringContainsT(t, body, `oauth2.auth.schema.get("flow") === "accessCode"`) }) t.Run("with explicit OAuth2CallbackURL", func(t *testing.T) { h := SwaggerUIOAuth2Callback(nil, WithSwaggerUIOptions(SwaggerUIOptions{ OAuth2CallbackURL: "/custom/callback", })) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/custom/callback", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) }) t.Run("with alternate base path and path", func(t *testing.T) { h := SwaggerUIOAuth2Callback(nil, WithUIBasePath("/api"), WithUIPath("ui"), ) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/api/ui/oauth2-callback", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) }) t.Run("returns 404 when no next handler", func(t *testing.T) { h := SwaggerUIOAuth2Callback(nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/nowhere", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusNotFound, recorder.Code) }) t.Run("falls through to next handler", func(t *testing.T) { next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusTeapot) }) h := SwaggerUIOAuth2Callback(next) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/elsewhere", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusTeapot, recorder.Code) }) t.Run("edge cases", func(t *testing.T) { t.Run("with template that fails to execute", func(t *testing.T) { assert.Panics(t, func() { SwaggerUIOAuth2Callback(nil, WithUITemplate(badTemplate)) }) }) }) } func TestUseSwaggerUIOAuth2Callback(t *testing.T) { t.Run("composes as a func(http.Handler) http.Handler", func(t *testing.T) { next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusTeapot) }) h := UseSwaggerUIOAuth2Callback()(next) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs/oauth2-callback", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) }) } go-openapi-runtime-decad8f/server-middleware/docui/swaggerui_test.go000066400000000000000000000130531520232310000261660ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package docui import ( "context" "fmt" "net/http" "net/http/httptest" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestSwaggerUIMiddleware(t *testing.T) { t.Run("with defaults", func(t *testing.T) { h := SwaggerUI(nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) require.NoError(t, err) recorder := httptest.NewRecorder() h.ServeHTTP(recorder, req) assert.EqualT(t, http.StatusOK, recorder.Code) assert.EqualT(t, "text/html; charset=utf-8", recorder.Header().Get(contentTypeHeader)) body := recorder.Body.String() assert.StringContainsT(t, body, fmt.Sprintf("%s", defaultDocsTitle)) // html/template JS-escapes '/' as '\/' inside