vendor: update buildkit to v0.31.0-rc2

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax committed 2026-06-12 13:43:37 +02:00
1 parent d244804641
commit c1ed6b6b2d
407 files changed
+16324 -4670

No files matched your search

+50 -47
View File
@@ -61,7 +61,7 @@ require (
github.com/miekg/dns v1.1.72
github.com/mistifyio/go-zfs/v4 v4.0.0
github.com/mitchellh/copystructure v1.2.0
github.com/moby/buildkit v0.31.0-rc1
github.com/moby/buildkit v0.31.0-rc2
github.com/moby/docker-image-spec v1.3.1
github.com/moby/go-archive v0.2.0
github.com/moby/ipvs v1.1.0
@@ -69,7 +69,7 @@ require (
github.com/moby/moby/api v1.54.2
github.com/moby/moby/client v0.4.1
github.com/moby/patternmatcher v0.6.1
github.com/moby/policy-helpers v0.0.0-20260507153417-a39d60132186
github.com/moby/policy-helpers v0.0.0-20260612073044-d5411a945cfc
github.com/moby/profiles/apparmor v0.2.1
github.com/moby/profiles/seccomp v0.2.3
github.com/moby/pubsub v1.0.0
@@ -125,13 +125,13 @@ require (
require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.19.0 // indirect
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/longrunning v1.0.0 // indirect
code.cloudfoundry.org/clock v1.60.0 // indirect
cyphar.com/go-pathrs v0.2.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
@@ -182,39 +182,40 @@ require (
github.com/fsnotify/fsnotify v1.9.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.24.3 // indirect
github.com/go-openapi/analysis v0.25.2 // indirect
github.com/go-openapi/errors v0.22.7 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/jsonpointer v0.23.1 // indirect
github.com/go-openapi/jsonreference v0.21.6 // indirect
github.com/go-openapi/loads v0.23.3 // indirect
github.com/go-openapi/runtime v0.29.3 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/strfmt v0.26.1 // indirect
github.com/go-openapi/swag v0.25.5 // indirect
github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/fileutils v0.25.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/mangling v0.25.5 // indirect
github.com/go-openapi/swag/netutils v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-openapi/validate v0.25.2 // indirect
github.com/go-openapi/runtime v0.32.3 // indirect
github.com/go-openapi/runtime/server-middleware v0.30.0 // indirect
github.com/go-openapi/spec v0.22.5 // indirect
github.com/go-openapi/strfmt v0.26.3 // indirect
github.com/go-openapi/swag v0.26.0 // indirect
github.com/go-openapi/swag/cmdutils v0.26.0 // 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/netutils 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.3 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/go-containerregistry v0.20.7 // indirect
github.com/google/certificate-transparency-go v1.3.3 // indirect
github.com/google/go-containerregistry v0.21.6 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -226,7 +227,7 @@ require (
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hiddeco/sshsig v0.2.0 // indirect
github.com/in-toto/attestation v1.1.2 // indirect
github.com/in-toto/attestation v1.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
@@ -245,22 +246,22 @@ require (
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/sasha-s/go-deadlock v0.3.5 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/sigstore/protobuf-specs v0.5.0 // indirect
github.com/sigstore/rekor v1.5.0 // indirect
github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect
github.com/sigstore/sigstore v1.10.5 // indirect
github.com/sigstore/sigstore-go v1.1.4 // indirect
github.com/sigstore/timestamp-authority/v2 v2.0.6 // indirect
github.com/sigstore/protobuf-specs v0.5.1 // indirect
github.com/sigstore/rekor v1.5.2 // indirect
github.com/sigstore/rekor-tiles/v2 v2.2.2-0.20260601073857-5d098a2b6443 // indirect
github.com/sigstore/sigstore v1.10.8 // indirect
github.com/sigstore/sigstore-go v1.2.1 // indirect
github.com/sigstore/timestamp-authority/v2 v2.1.2 // indirect
github.com/spdx/tools-golang v0.5.7 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect
github.com/theupdateframework/go-tuf/v2 v2.4.1 // indirect
github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
github.com/tonistiigi/fsutil v0.0.0-20260609091201-0257b3308df4 // indirect
@@ -268,15 +269,16 @@ require (
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect
github.com/transparency-dev/formats v0.1.1 // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
github.com/weppos/publicsuffix-go v0.30.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 // indirect
github.com/zmap/zlint/v3 v3.5.0 // indirect
go.etcd.io/etcd/api/v3 v3.6.6 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect
go.etcd.io/etcd/pkg/v3 v3.6.6 // indirect
go.etcd.io/etcd/server/v3 v3.6.6 // indirect
go.etcd.io/etcd/api/v3 v3.6.8 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.8 // indirect
go.etcd.io/etcd/pkg/v3 v3.6.8 // indirect
go.etcd.io/etcd/server/v3 v3.6.8 // indirect
go.etcd.io/raft/v3 v3.6.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
@@ -291,7 +293,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.uber.org/zap v1.28.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.53.0 // indirect
@@ -299,10 +301,11 @@ require (
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/tools v0.45.0 // indirect
google.golang.org/api v0.274.0 // indirect
google.golang.org/api v0.280.0 // indirect
google.golang.org/genproto v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
tags.cncf.io/container-device-interface/specs-go v1.1.0 // indirect
)
+141 -146
View File
@@ -3,8 +3,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.19.0 h1:DGYwtbcsGsT1ywuxsIoWi1u/vlks0moIblQHgSDgQkQ=
cloud.google.com/go/auth v0.19.0/go.mod h1:2Aph7BT2KnaSFOM0JDPyiYgNh6PL9vGMiP8CUIXZ+IY=
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
@@ -26,32 +26,35 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e h1:VsUbObBMxXlc23Eb9VeeJYE4jvTs87qa5RqSN2U5FJU=
filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e/go.mod h1:32qQ5yj3R24Eu03iWFWchdC3OB653wPvoepWejkefbY=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg=
github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0 h1:MaKvxE6D0KkjOg6Wd9M00iqP5PR0kUxCfiezes4JweM=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0/go.mod h1:i2h9fsTFKZorh8RdV2IcSUf/Qj98GlTkrTvUbX/s8as=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 h1:mlmW46Q0B79I+Aj4azKC6xDMFN9a9SyZWESlGWYXbFs=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0/go.mod h1:PXe2h+LKcWTX9afWdZoHyODqR4fBa5boUM/8uJfZ0Jo=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Graylog2/go-gelf v0.0.0-20191017102106-1550ee647df0 h1:cOjLyhBhe91glgZZNbQUg9BJC57l6BiSKov0Ivv7k0U=
github.com/Graylog2/go-gelf v0.0.0-20191017102106-1550ee647df0/go.mod h1:fBaQWrftOD5CrVCUfoYGHs4X4VViTuGOXA8WloCjTY0=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@@ -79,8 +82,6 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g=
@@ -103,8 +104,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BS
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw=
github.com/aws/aws-sdk-go-v2/service/kms v1.52.0 h1:QNtg+Mtj1zmepk568+UKBD5DFfqh+ESTUUqQT27JkQc=
github.com/aws/aws-sdk-go-v2/service/kms v1.52.0/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI=
github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 h1:6Xt6Ztjkwdia/7EtEaG7ki/qZUYlCcd7tGUotQed1QE=
github.com/aws/aws-sdk-go-v2/service/signin v1.1.5/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo=
@@ -263,8 +264,8 @@ github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhs
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -278,63 +279,65 @@ 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.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk=
github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw=
github.com/go-openapi/analysis v0.25.2 h1:I0vy4n3alz+DHTiN1PRhCb7QZxkK6g5YmswZKv2TKuw=
github.com/go-openapi/analysis v0.25.2/go.mod h1:Uhs1t/2XR10EnwONYILGEzw8gcfGIG5Xk5K2AxnhqDo=
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.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
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/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.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y=
github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY=
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/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y=
github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI=
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.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c=
github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=
github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=
github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=
github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c=
github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=
github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=
github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=
github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU=
github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q=
github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw=
github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
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-openapi/runtime v0.32.3 h1:J7Ycy5DJmhhP1By3NifhRUjnkXTrk21qbeqSULjwX8U=
github.com/go-openapi/runtime v0.32.3/go.mod h1:/WTQi0fa5DiGnnCXQKsTkSm15OzJp8Uz3H2t+67TBr4=
github.com/go-openapi/runtime/server-middleware v0.30.0 h1:8rPoJ/xv7JL8BsovaqboKETlpWBArVh8n+0L/GyePog=
github.com/go-openapi/runtime/server-middleware v0.30.0/go.mod h1:OYNT/TxNvB/VK5oe4htM2jDTwlEXuejVJmu0DVZfAMs=
github.com/go-openapi/spec v0.22.5 h1:KhO7RBlKQfonUWX2WzQCoLIXVA6AcNqDGZ3a1Dutdlo=
github.com/go-openapi/spec v0.22.5/go.mod h1:vxpOtMya5TXtENXKE5bKqv5NjocVhyhxHrlZfvKnZ74=
github.com/go-openapi/strfmt v0.26.3 h1:rzmslHarJgBbf2qfGge+X3htclQfmXqBZMm0Too0HhU=
github.com/go-openapi/strfmt v0.26.3/go.mod h1:a5nsUw0oRpQzZeOwx8bi6cKbzFZslpbCKt1LEot+KnQ=
github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI=
github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0=
github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU=
github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
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/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c=
github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo=
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.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8=
github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk=
github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo=
github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-openapi/validate v0.25.3 h1:4nzAIavcJ7WveHK2+V1UAkZK3kWcjzxZCzjfZAfavKs=
github.com/go-openapi/validate v0.25.3/go.mod h1:GemfuGMyYpIaBoKpX3z8sLywrmxpzWVOoJ7R0VeAVuk=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
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/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
@@ -344,8 +347,8 @@ github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -370,8 +373,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs=
github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo=
github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA=
github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -383,28 +386,28 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
github.com/google/go-containerregistry v0.21.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM=
github.com/google/go-containerregistry v0.21.6/go.mod h1:U7MMSBIJynke2MVQrQk19NP9k/uQsGz/h0amIFSHMbo=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno=
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js=
github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81zxNQw05s=
github.com/google/trillian v1.7.3 h1:hziW+vo4czis48tzx2GK5xRBl/ZxBA9B0/UR5avXOro=
github.com/google/trillian v1.7.3/go.mod h1:qh8iy4x/GvnVXUBd5pK4oncuT1Y9vVYfibQVsR/WpKg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
@@ -466,8 +469,8 @@ github.com/hiddeco/sshsig v0.2.0 h1:gMWllgKCITXdydVkDL+Zro0PU96QI55LwUwebSwNTSw=
github.com/hiddeco/sshsig v0.2.0/go.mod h1:nJc98aGgiH6Yql2doqH4CTBVHexQA40Q+hMMLHP4EqE=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E=
github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM=
github.com/in-toto/attestation v1.2.0 h1:aPRUZ3azbqD7yEBD5fP3TD8Dszf+YHo284SOcpahjQk=
github.com/in-toto/attestation v1.2.0/go.mod h1:r79G45gOmzPismgObLSL+rZTFxUgZLOQJI6LofTZgXk=
github.com/in-toto/in-toto-golang v0.11.0 h1:nfidMYBFx+E0lnmX5KUnN2Pdm8zdNKal1ayjJuzzRoA=
github.com/in-toto/in-toto-golang v0.11.0/go.mod h1:u3PjTnwFKjp5a1YCcw8SJg0G+tMeKfVoWsWeFMDCMtw=
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
@@ -475,20 +478,10 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2 h1:36qep4gxKs+JgeHGWeQ040RyZdt9kQlLglL1rFVn/oQ=
github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8=
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU=
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY=
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@@ -522,8 +515,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/letsencrypt/boulder v0.20260223.0 h1:xdS2OnJNUasR6TgVIOpqqcvdkOu47+PQQMBk9ThuWBw=
github.com/letsencrypt/boulder v0.20260223.0/go.mod h1:r3aTSA7UZ7dbDfiGK+HLHJz0bWNbHk6YSPiXgzl23sA=
github.com/letsencrypt/boulder v0.20260309.0 h1:kZynrxK3QfqLGx6hhoz+Rfs3hgltJs1p9Mp+4+VwnY0=
github.com/letsencrypt/boulder v0.20260309.0/go.mod h1:yG8lj8pNPZ8taq3oNdTpfBS+eC74IaEuiewqzVpXiWE=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -549,8 +542,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/buildkit v0.31.0-rc1 h1:XAsHnDZHw+LrZNFubvCtIP15SO011oKhpP3Vom5e6zA=
github.com/moby/buildkit v0.31.0-rc1/go.mod h1:9NQbhPKN8TizS1PFglCTHotCdHXF3Vm36FR5t3fjJ38=
github.com/moby/buildkit v0.31.0-rc2 h1:HeMIJhy7KVXdc+Qg/4ZGwBFEdeZon7V1ddULCMHMvt4=
github.com/moby/buildkit v0.31.0-rc2/go.mod h1:YM5iNEbNCc6L1Zt3YWFB/aXNLufvf4Rcu0DPlc9HwQg=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
@@ -561,8 +554,8 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/policy-helpers v0.0.0-20260507153417-a39d60132186 h1:AA3y9usJgmJyrOX16s8HsgHA3QP0CSvI9EJ9vVmpgGo=
github.com/moby/policy-helpers v0.0.0-20260507153417-a39d60132186/go.mod h1:AbesLhDyQnWkhYOeG5BjDpfUWuyFlSpwv17zBGc03ag=
github.com/moby/policy-helpers v0.0.0-20260612073044-d5411a945cfc h1:dvhPFj1niuMP3CBCjhiZWJQr//+w1LOA8cHclFJnNe0=
github.com/moby/policy-helpers v0.0.0-20260612073044-d5411a945cfc/go.mod h1:frGYJTxenVCGPa9doaqZSU9FqzT7bt+1dFeVAaCFoyQ=
github.com/moby/profiles/apparmor v0.2.1 h1:LAUrERjl2trYlInOppli35RQJPIfR3KMWQYeNFTd4+s=
github.com/moby/profiles/apparmor v0.2.1/go.mod h1:DtaWKVB992B38PG5i6VZU6w71JQatA6qtkaQGiqUsoc=
github.com/moby/profiles/seccomp v0.2.3 h1:nrHNSiECQQvq4WjgceCUgJXIXUJBIswVQ133k4Do2mA=
@@ -676,16 +669,16 @@ github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
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/rootless-containers/rootlesskit/v3 v3.0.1 h1:AbYUo1O3b8YFL9X8YuEaaWkVFTwdU551CKyuOt0nJok=
@@ -701,32 +694,32 @@ github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgm
github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/secure-systems-lab/go-securesystemslib v0.10.0 h1:l+H5ErcW0PAehBNrBxoGv1jjNpGYdZ9RcheFkB2WI14=
github.com/secure-systems-lab/go-securesystemslib v0.10.0/go.mod h1:MRKONWmRoFzPNQ9USRF9i1mc7MvAVvF1LlW8X5VWDvk=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs=
github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY=
github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
github.com/sigstore/rekor v1.5.0 h1:rL7SghHd5HLCtsCrxw0yQg+NczGvM75EjSPPWuGjaiQ=
github.com/sigstore/rekor v1.5.0/go.mod h1:D7JoVCUkxwQOpPDNYeu+CE8zeBC18Y5uDo6tF8s2rcQ=
github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo=
github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU=
github.com/sigstore/sigstore v1.10.5 h1:KqrOjDhNOVY+uOzQFat2FrGLClPPCb3uz8pK3wuI+ow=
github.com/sigstore/sigstore v1.10.5/go.mod h1:k/mcVVXw3I87dYG/iCVTSW2xTrW7vPzxxGic4KqsqXs=
github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg=
github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.5 h1:aqHRubTITULckG9JAcq2FEhtKkT/RRE8oErfuV3smSI=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.5/go.mod h1:h9eK9QyPqpFskF/ewFkRLtwh4/Q3FLc2/DXbym4IHN8=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.5 h1:+9C6CUkv+J4iT67Lx+H1EGBfAdoAHqXumHadeIj9jA4=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.5/go.mod h1:myZsg7wRiy/vf102g5uUAitYhtXCwepmAGxgHG1VHuE=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.5 h1:BpQx6AhjwIN9LmlO4ypkcMcHiWiepgZQGSw5U69frHU=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.5/go.mod h1:ejMD/17lMJ4HykQRPdj5NNr+OQYIEZto8HjDKghVMOA=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.5 h1:OFwQZgWkB/6J6W5sy3SkXE4pJnhNRnE2cJd8ySXmHpo=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.5/go.mod h1:Ee/enmyxi/RFLVlajbnjgH2wOWQwlJ0wY8qZrk43hEw=
github.com/sigstore/timestamp-authority/v2 v2.0.6 h1:1Vh7/SdmLsVLG6Br6/bisd1SnlicfDm0MJYiA+D7Ppw=
github.com/sigstore/timestamp-authority/v2 v2.0.6/go.mod h1:Nk5ucGBDyH0tXAIMZ0prf6xn8qfTnbJhSq+CDabYcfc=
github.com/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah1/+7Px7U=
github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats=
github.com/sigstore/rekor v1.5.2 h1:k6pX4o1zFAzAvDbXiVIp5IHj1b0wcDaxsbsbNpuRO8o=
github.com/sigstore/rekor v1.5.2/go.mod h1:WkMnITBccOFauPkT6yte74tF5gC83pefKRGZvNOsbjI=
github.com/sigstore/rekor-tiles/v2 v2.2.2-0.20260601073857-5d098a2b6443 h1:/CO8F6m3Bo/f59bZo5dv1sTIfUnQqVnepIdDV24KoDw=
github.com/sigstore/rekor-tiles/v2 v2.2.2-0.20260601073857-5d098a2b6443/go.mod h1:w1h8wF8vq9lHjmtRdwJiEaoVxhP+WHIMpj4M39pkzp0=
github.com/sigstore/sigstore v1.10.8 h1:1Mgkxvkw4AXMfIP1DOjc6kw0GkUgA8pGVpveN/EfOq4=
github.com/sigstore/sigstore v1.10.8/go.mod h1:f9+B/4iaYimvUkySyb2mvc73n3RLqNn24grHZM/ET8M=
github.com/sigstore/sigstore-go v1.2.1 h1:YWP/rDbBaEBvtbkj6xtwsSj38ZCFEhTVVadNOXjVe3A=
github.com/sigstore/sigstore-go v1.2.1/go.mod h1:I8BqVwAb/SaQJ5pBu5IDFY+ksq8O/1/kCag8XUgrsko=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.8 h1:tofVQ+UWJgad/69I5zbqxdFCN5gpIn9tRQP7iBzIpBw=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.8/go.mod h1:73AfJE8H6w5KGCFPBu4x/OG+i1Yxgmh0L/FtV7prd88=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.8 h1:8Mt7J36GcUEmbiJaiFhz2tud5ZIgkfVVCe2H/WJCHmw=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.8/go.mod h1:YiTpAsxoWXhF9KlLOVWCh7BckN5cYO8X01WufDq1ido=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.8 h1:MxpAIMZVzn0Tpbarc9ax1I498oQBp7oYSMgoMSsOmKI=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.8/go.mod h1:bnAUEkFNam6STvkVZhptVwWzWR5pS24CEtQ+lhxu7S0=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8 h1:1DGe4/clcdOnkz5MINEczWlmEvjUtZd+AjPPT/cBhQ8=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8/go.mod h1:6IDFhpgxtzqbnzrFkyegbj7RfWwKeRrb3/+xAD1Wp+Y=
github.com/sigstore/timestamp-authority/v2 v2.1.2 h1:7DDhnknLL4w8VwomyvW2W8qblOS9LDR8oihna+jc7Ls=
github.com/sigstore/timestamp-authority/v2 v2.1.2/go.mod h1:o6rAVZceFyejClIj/uStRNIemP16bVMZtbMmhk6pr0U=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -768,14 +761,14 @@ github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbw
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug=
github.com/theupdateframework/go-tuf/v2 v2.4.1 h1:K6ewW064rKZCPkRo1W/CTbTtm/+IB4+coG1iNURAGCw=
github.com/theupdateframework/go-tuf/v2 v2.4.1/go.mod h1:Nex2enPVYDFCklrnbTzl3OVwD7fgIAj0J5++z/rvCj8=
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI=
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis=
github.com/theupdateframework/go-tuf/v2 v2.4.2 h1:w7976/W8uTwlsegP5nRymlpjPgrwSh+AXUf85is6nJk=
github.com/theupdateframework/go-tuf/v2 v2.4.2/go.mod h1:JqBrIUnNLAaNq/8GmBcEMFWfAFBbqp/MkJEJseXKbks=
github.com/tink-crypto/tink-go-awskms/v3 v3.0.0 h1:XSohRhCkXAVI0iaCnWB/GS05TEmpnKurQmzaY1jzt3Y=
github.com/tink-crypto/tink-go-awskms/v3 v3.0.0/go.mod h1:+7MXsShLzVbSQ6dI0Pe4JuZM52jD1jQ1itAygd/MDsA=
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0=
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw=
github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0 h1:j+S+WKBQ5ya26A5EM/uXoVe+a2IaPQN8KgBJZ22cJ+4=
github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0/go.mod h1:OCKJIujnTzDq7f+73NhVs99oA2c1TR6nsOpuasYM6Yo=
github.com/tink-crypto/tink-go-hcvault/v2 v2.5.0 h1:eXuNqgrcYelxU1MVikOJDP3wTS5lvihM4ntoAbAMfvs=
github.com/tink-crypto/tink-go-hcvault/v2 v2.5.0/go.mod h1:3RhcxAqek6xUlRFmJifvU4CYLZN60KMQdIKqpZAZJG0=
github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4=
github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
@@ -796,8 +789,8 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw=
github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI=
github.com/transparency-dev/formats v0.1.1 h1:4bVHJc+KdBgpA1OJD1yjI+g0i5Z1graCppTMH8lWKJI=
github.com/transparency-dev/formats v0.1.1/go.mod h1:qtZ8goRuJ8FTBG9c9+Bj0rn2rUG7eG/AUTkr+Aw3jFw=
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
@@ -818,6 +811,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -836,14 +831,14 @@ github.com/zmap/zlint/v3 v3.5.0 h1:Eh2B5t6VKgVH0DFmTwOqE50POvyDhUaU9T2mJOe1vfQ=
github.com/zmap/zlint/v3 v3.5.0/go.mod h1:JkNSrsDJ8F4VRtBZcYUQSvnWFL7utcjDIn+FE64mlBI=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ=
go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4=
go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs=
go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI=
go.etcd.io/etcd/pkg/v3 v3.6.6 h1:wylOivS/UxXTZ0Le5fOdxCjatW5ql9dcWEggQQHSorw=
go.etcd.io/etcd/pkg/v3 v3.6.6/go.mod h1:9TKZL7WUEVHXYM3srP3ESZfIms34s1G72eNtWA9YKg4=
go.etcd.io/etcd/server/v3 v3.6.6 h1:YSRWGJPzU+lIREwUQI4MfyLZrkUyzjJOVpMxJvZePaY=
go.etcd.io/etcd/server/v3 v3.6.6/go.mod h1:A1OQ1x3PaiENDLywMjCiMwV1pwJSpb0h9Z5ORP2dv6I=
go.etcd.io/etcd/api/v3 v3.6.8 h1:gqb1VN92TAI6G2FiBvWcqKtHiIjr4SU2GdXxTwyexbM=
go.etcd.io/etcd/api/v3 v3.6.8/go.mod h1:qyQj1HZPUV3B5cbAL8scG62+fyz5dSxxu0w8pn28N6Q=
go.etcd.io/etcd/client/pkg/v3 v3.6.8 h1:Qs/5C0LNFiqXxYf2GU8MVjYUEXJ6sZaYOz0zEqQgy50=
go.etcd.io/etcd/client/pkg/v3 v3.6.8/go.mod h1:GsiTRUZE2318PggZkAo6sWb6l8JLVrnckTNfbG8PWtw=
go.etcd.io/etcd/pkg/v3 v3.6.8 h1:Xe+LIL974spy8b4nEx3H0KMr1ofq3r0kh6FbU3aw4es=
go.etcd.io/etcd/pkg/v3 v3.6.8/go.mod h1:TRibVNe+FqJIe1abOAA1PsuQ4wqO87ZaOoprg09Tn8c=
go.etcd.io/etcd/server/v3 v3.6.8 h1:U2strdSEy1U8qcSzRIdkYpvOPtBy/9i/IfaaCI9flZ4=
go.etcd.io/etcd/server/v3 v3.6.8/go.mod h1:88dCtwUnSirkUoJbflQxxWXqtBSZa6lSG0Kuej+dois=
go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
@@ -888,14 +883,14 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.step.sm/crypto v0.77.2 h1:qFjjei+RHc5kP5R7NW9OUWT7SqWIuAOvOkXqg4fNWj8=
go.step.sm/crypto v0.77.2/go.mod h1:W0YJb9onM5l78qgkXIJ2Up6grnwW8EtpCKIza/NCg0o=
go.step.sm/crypto v0.77.7 h1:6azC+pD678Vjju8yXnMDHCZJ+HzFaEmL3sCryiezTIA=
go.step.sm/crypto v0.77.7/go.mod h1:OW/2sEHwTtDKq70PvSQ5B0JGy/CrLyDKOiVy3YvZMTQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@@ -1030,8 +1025,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.274.0 h1:aYhycS5QQCwxHLwfEHRRLf9yNsfvp1JadKKWBE54RFA=
google.golang.org/api v0.274.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew=
google.golang.org/api v0.280.0 h1:F4OfEHZhZh6a7uTufJAXXVd/2TQ8EjM4vZH+jX/vFYk=
google.golang.org/api v0.280.0/go.mod h1:oGKmPZRDoD3vdkf6MA7F4VNkR1rxCiuaPSkhsf3EolU=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
@@ -1086,8 +1081,8 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.78 h1:jgqg4gyu2BaYW9L6uzEtGLf8GNREwk/z4UFdwt5F3pE=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.78/go.mod h1:VjuVda6m2qGkpCVfrFkpTGyvkdlZ2N5/yfo89tujlg8=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.78 h1:PC3yNs51cX5LZ7U57a7xielBcoXB3xnV+rXD8V0H0DQ=
+2
View File
@@ -1,5 +1,7 @@
# Changes
## [0.20.0](https://github.com/googleapis/google-cloud-go/releases/tag/auth%2Fv0.20.0) (2026-04-06)
## [0.19.0](https://github.com/googleapis/google-cloud-go/releases/tag/auth%2Fv0.19.0) (2026-03-23)
### Features
+155 -56
View File
@@ -22,9 +22,11 @@ import (
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"strconv"
"strings"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
@@ -38,11 +40,9 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
grpccreds "google.golang.org/grpc/credentials"
grpcinsecure "google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/stats"
"google.golang.org/grpc/status"
)
const (
@@ -370,7 +370,7 @@ func dial(ctx context.Context, secure bool, opts *Options) (*grpc.ClientConn, er
// Add tracing, but before the other options, so that clients can override the
// gRPC stats handler.
// This assumes that gRPC options are processed in order, left to right.
grpcOpts = addOpenTelemetryStatsHandler(grpcOpts, opts)
grpcOpts = addOpenTelemetryStatsHandler(grpcOpts, opts, transportCreds.Endpoint)
grpcOpts = append(grpcOpts, opts.GRPCDialOpts...)
return grpc.DialContext(ctx, transportCreds.Endpoint, grpcOpts...)
@@ -458,29 +458,89 @@ func (c *grpcCredentialsProvider) RequireTransportSecurity() bool {
return c.secure
}
func addOpenTelemetryStatsHandler(dialOpts []grpc.DialOption, opts *Options) []grpc.DialOption {
func addOpenTelemetryStatsHandler(dialOpts []grpc.DialOption, opts *Options, endpoint string) []grpc.DialOption {
if opts.DisableTelemetry {
return dialOpts
}
if !gax.IsFeatureEnabled("TRACING") {
if gax.IsFeatureEnabled("METRICS") {
host, port := extractHostPort(endpoint)
dialOpts = append(dialOpts, grpc.WithChainUnaryInterceptor(openTelemetryUnaryClientInterceptor(host, port)))
}
if !gax.IsFeatureEnabled("TRACING") && !gax.IsFeatureEnabled("LOGGING") {
return append(dialOpts, grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
}
var staticAttrs []attribute.KeyValue
var scopedLogger *slog.Logger
if gax.IsFeatureEnabled("LOGGING") && opts.Logger != nil {
scopedLogger = opts.Logger
}
if opts.InternalOptions != nil {
staticAttrs = transport.StaticTelemetryAttributes(opts.InternalOptions.TelemetryAttributes)
if scopedLogger != nil {
var staticLogAttrs []any
for _, attr := range staticAttrs {
staticLogAttrs = append(staticLogAttrs, slog.String(string(attr.Key), attr.Value.AsString()))
}
scopedLogger = scopedLogger.With(staticLogAttrs...)
}
}
otelOpts := []otelgrpc.Option{
otelgrpc.WithSpanAttributes(staticAttrs...),
var otelOpts []otelgrpc.Option
if gax.IsFeatureEnabled("TRACING") {
otelOpts = append(otelOpts, otelgrpc.WithSpanAttributes(staticAttrs...))
}
return append(dialOpts, grpc.WithStatsHandler(&otelHandler{
Handler: otelgrpc.NewClientHandler(otelOpts...),
logger: scopedLogger,
}))
}
// Extract the host and port from a target address
func extractHostPort(target string) (string, int) {
if idx := strings.Index(target, "://"); idx != -1 {
target = target[idx+3:]
// Ensure any leading authorities (like 8.8.8.8 in dns://8.8.8.8/foo) are stripped
if slashIdx := strings.Index(target, "/"); slashIdx != -1 {
target = target[slashIdx+1:]
}
}
host, portStr, err := net.SplitHostPort(target)
if err != nil {
return target, 0
}
port, err := strconv.Atoi(portStr)
if err != nil {
return host, 0
}
return host, port
}
// openTelemetryUnaryClientInterceptor returns an interceptor that populates
// TransportTelemetryData with the server peer address.
func openTelemetryUnaryClientInterceptor(host string, port int) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
transportData := gax.ExtractTransportTelemetry(ctx)
if transportData != nil {
if host != "" {
transportData.SetServerAddress(host)
}
if port != 0 {
transportData.SetServerPort(port)
}
}
err := invoker(ctx, method, req, reply, cc, opts...)
return err
}
}
// otelHandler is a wrapper around the OpenTelemetry gRPC client handler that
// adds custom Google Cloud-specific attributes to spans and metrics.
type otelHandler struct {
stats.Handler
logger *slog.Logger
}
// TagRPC intercepts the RPC start to extract dynamic attributes like resource
@@ -488,8 +548,15 @@ type otelHandler struct {
// the current span.
func (h *otelHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {
ctx = h.Handler.TagRPC(ctx, info)
span := trace.SpanFromContext(ctx)
if !span.IsRecording() {
var span trace.Span
if gax.IsFeatureEnabled("TRACING") {
if s := trace.SpanFromContext(ctx); s != nil && s.IsRecording() {
span = s
}
}
if span == nil {
return ctx
}
var attrs []attribute.KeyValue
@@ -515,62 +582,94 @@ func (h *otelHandler) HandleRPC(ctx context.Context, s stats.RPCStats) {
h.Handler.HandleRPC(ctx, s)
return
}
span := trace.SpanFromContext(ctx)
if !span.IsRecording() {
var span trace.Span
if gax.IsFeatureEnabled("TRACING") {
if s := trace.SpanFromContext(ctx); s != nil && s.IsRecording() {
span = s
}
}
var logger *slog.Logger
if gax.IsFeatureEnabled("LOGGING") {
if l := h.logger; l != nil && l.Enabled(ctx, slog.LevelDebug) {
logger = l
}
}
if span == nil && logger == nil {
h.Handler.HandleRPC(ctx, s)
return
}
var attrs []attribute.KeyValue
if end.Error != nil {
st, ok := status.FromError(end.Error)
rpcStatusCode := codeToCanonicalStr(st.Code())
var errorType string
// 1. Check if the local context expired or was cancelled. This is the only
// reliable way to distinguish a local client timeout from a server timeout
// because gRPC does not wrap context errors in its status.Error types.
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
errorType = "CLIENT_TIMEOUT"
} else if errors.Is(ctx.Err(), context.Canceled) {
errorType = "CLIENT_CANCELLED"
} else if !ok || st.Code() == codes.Unknown || st.Code() == codes.Internal {
// 2. If the error isn't a context breakdown and the gRPC framework
// doesn't "understand" it (returning ok=false or a generic catch-all
// bucket like Unknown/Internal), we "pack" the actual Go error type
// name into error.type (e.g., "*net.OpError"). This is per the error.type
// [spec](https://opentelemetry.io/docs/specs/semconv/registry/attributes/error/#error-type).
// "When error.type is set to a type (e.g., an exception type), its canonical
// class name identifying the type within the artifact SHOULD be used."
errorType = fmt.Sprintf("%T", end.Error)
} else {
// 3. Otherwise, it is a well-understood gRPC protocol error (e.g.,
// PERMISSION_DENIED) likely returned by the server.
errorType = rpcStatusCode
}
attrs = []attribute.KeyValue{
attribute.String("error.type", errorType),
attribute.String("status.message", st.Message()),
attribute.String("rpc.response.status_code", rpcStatusCode),
attribute.String("exception.type", fmt.Sprintf("%T", end.Error)),
}
h.handleRPCError(ctx, span, logger, end)
} else {
attrs = []attribute.KeyValue{
attribute.String("rpc.response.status_code", "OK"),
}
h.handleRPCSuccess(span)
}
span.SetAttributes(attrs...)
h.Handler.HandleRPC(ctx, s)
}
// codeToCanonicalStr returns the canonical name for each of the 17 gRPC
// status codes defined in https://github.com/grpc/grpc-go/blob/master/codes/codes.go.
// For any codes.Code that converts to an out-of-bounds int,
// it returns "UNKNOWN".
func codeToCanonicalStr(code codes.Code) string {
if int(code) >= 0 && int(code) < len(codeToStr) {
return codeToStr[code]
func (h *otelHandler) handleRPCError(ctx context.Context, span trace.Span, logger *slog.Logger, end *stats.End) {
info := gax.ExtractTelemetryErrorInfo(ctx, end.Error)
if logger != nil {
logActionableError(ctx, logger, info)
}
if span != nil {
attrs := []attribute.KeyValue{
attribute.String("error.type", info.ErrorType),
attribute.String("status.message", info.StatusMessage),
attribute.String("rpc.response.status_code", info.StatusCode),
attribute.String("exception.type", fmt.Sprintf("%T", end.Error)),
}
span.SetAttributes(attrs...)
}
return "UNKNOWN"
}
func (h *otelHandler) handleRPCSuccess(span trace.Span) {
if span != nil {
attrs := []attribute.KeyValue{
attribute.String("rpc.response.status_code", "OK"),
}
span.SetAttributes(attrs...)
}
}
func logActionableError(ctx context.Context, logger *slog.Logger, info gax.TelemetryErrorInfo) {
baseLogAttrs := []slog.Attr{
slog.String("rpc.system.name", "grpc"),
slog.String("rpc.response.status_code", info.StatusCode),
}
if resendCountStr, ok := callctx.TelemetryFromContext(ctx, "resend_count"); ok {
if count, e := strconv.Atoi(resendCountStr); e == nil {
baseLogAttrs = append(baseLogAttrs, slog.Int64("gcp.grpc.resend_count", int64(count)))
}
}
msg := info.StatusMessage
if msg == "" {
msg = "API call failed"
}
if rpcMethod, ok := callctx.TelemetryFromContext(ctx, "rpc_method"); ok {
baseLogAttrs = append(baseLogAttrs, slog.String("rpc.method", rpcMethod))
}
if resName, ok := callctx.TelemetryFromContext(ctx, "resource_name"); ok {
baseLogAttrs = append(baseLogAttrs, slog.String("gcp.resource.destination.id", resName))
}
if info.Domain != "" {
baseLogAttrs = append(baseLogAttrs, slog.String("gcp.errors.domain", info.Domain))
}
for k, v := range info.Metadata {
baseLogAttrs = append(baseLogAttrs, slog.String("gcp.errors.metadata."+k, v))
}
baseLogAttrs = append(baseLogAttrs, slog.String("error.type", info.ErrorType))
logger.LogAttrs(ctx, slog.LevelDebug, msg, baseLogAttrs...)
}
+271 -39
View File
@@ -15,14 +15,17 @@
package httptransport
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"os"
"strconv"
"sync"
"time"
"cloud.google.com/go/auth"
@@ -37,10 +40,12 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/net/http2"
"google.golang.org/api/googleapi"
)
const (
quotaProjectHeaderKey = "X-goog-user-project"
maxErrorReadBytes = int64(8192) // 8KB
)
func newTransport(base http.RoundTripper, opts *Options) (http.RoundTripper, error) {
@@ -180,33 +185,65 @@ func addOpenTelemetryTransport(trans http.RoundTripper, opts *Options) http.Roun
if opts.DisableTelemetry {
return trans
}
if !gax.IsFeatureEnabled("TRACING") {
var traceAttrs []attribute.KeyValue
var scopedLogger *slog.Logger
if gax.IsFeatureEnabled("LOGGING") && opts.Logger != nil {
scopedLogger = opts.Logger
}
if opts.InternalOptions != nil {
attrs := transport.StaticTelemetryAttributes(opts.InternalOptions.TelemetryAttributes)
if gax.IsFeatureEnabled("TRACING") {
traceAttrs = attrs
}
if scopedLogger != nil {
var logAttrs []any
for _, attr := range attrs {
logAttrs = append(logAttrs, slog.String(string(attr.Key), attr.Value.AsString()))
}
scopedLogger = scopedLogger.With(logAttrs...)
}
}
if gax.IsFeatureEnabled("METRICS") || gax.IsFeatureEnabled("TRACING") || gax.IsFeatureEnabled("LOGGING") {
trans = &otelAttributeTransport{
base: trans,
logger: scopedLogger,
}
}
if !gax.IsFeatureEnabled("TRACING") && !gax.IsFeatureEnabled("LOGGING") {
return otelhttp.NewTransport(trans)
}
var staticAttrs []attribute.KeyValue
if opts.InternalOptions != nil {
staticAttrs = transport.StaticTelemetryAttributes(opts.InternalOptions.TelemetryAttributes)
var otelOpts []otelhttp.Option
if len(traceAttrs) > 0 {
otelOpts = append(otelOpts, otelhttp.WithSpanOptions(trace.WithAttributes(traceAttrs...)))
}
otelOpts := []otelhttp.Option{
otelhttp.WithSpanOptions(trace.WithAttributes(staticAttrs...)),
}
return otelhttp.NewTransport(&otelAttributeTransport{
base: trans,
}, otelOpts...)
return otelhttp.NewTransport(trans, otelOpts...)
}
// otelAttributeTransport is a wrapper around an http.RoundTripper that adds
// custom Google Cloud-specific attributes to OpenTelemetry spans.
type otelAttributeTransport struct {
base http.RoundTripper
base http.RoundTripper
logger *slog.Logger
}
// RoundTrip intercepts the HTTP request and response to enrich the active
// OpenTelemetry span with static and dynamic attributes, as well as detailed
// error information.
func (t *otelAttributeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
span := trace.SpanFromContext(req.Context())
if span.IsRecording() {
var span trace.Span
if gax.IsFeatureEnabled("TRACING") {
if s := trace.SpanFromContext(req.Context()); s != nil && s.IsRecording() {
span = s
}
}
if span != nil {
var attrs []attribute.KeyValue
attrs = append(attrs, attribute.String("rpc.system.name", "http"))
if resName, ok := callctx.TelemetryFromContext(req.Context(), "resource_name"); ok {
@@ -224,39 +261,234 @@ func (t *otelAttributeTransport) RoundTrip(req *http.Request) (*http.Response, e
span.SetAttributes(attrs...)
}
resp, err := t.base.RoundTrip(req)
if span.IsRecording() {
if err != nil {
var errorType string
switch {
case errors.Is(err, context.DeadlineExceeded):
errorType = "CLIENT_TIMEOUT"
case errors.Is(err, context.Canceled):
errorType = "CLIENT_CANCELLED"
default:
errorType = "CLIENT_CONNECTION_ERROR"
var data *gax.TransportTelemetryData
if gax.IsFeatureEnabled("METRICS") {
data = gax.ExtractTransportTelemetry(req.Context())
if data != nil && req.URL != nil {
host := req.URL.Hostname()
if host != "" {
data.SetServerAddress(host)
}
span.SetAttributes(
attribute.String("error.type", errorType),
attribute.String("status.message", err.Error()),
attribute.String("exception.type", fmt.Sprintf("%T", err)),
)
} else {
span.SetAttributes(attribute.Int("http.response.status_code", resp.StatusCode))
if resp.StatusCode >= 400 {
errorType := strconv.Itoa(resp.StatusCode)
span.SetAttributes(
attribute.String("error.type", errorType),
attribute.String("status.message", resp.Status),
)
portStr := req.URL.Port()
if portStr == "" {
if req.URL.Scheme == "https" {
portStr = "443"
} else if req.URL.Scheme == "http" {
portStr = "80"
}
}
if port, pErr := strconv.Atoi(portStr); pErr == nil {
data.SetServerPort(port)
}
}
}
resp, err := t.base.RoundTrip(req)
var logger *slog.Logger
if gax.IsFeatureEnabled("LOGGING") {
if l := t.logger; l != nil && l.Enabled(req.Context(), slog.LevelDebug) {
logger = l
}
}
if span == nil && logger == nil {
return resp, err
}
if err != nil {
t.logAndSpanError(req, resp, err, err, span, logger)
} else if resp.StatusCode >= 400 {
if resp.Body != nil && resp.Body != http.NoBody && (resp.ContentLength < 0 || resp.ContentLength <= maxErrorReadBytes) {
resp.Body = &errorTrackingBody{
ReadCloser: resp.Body,
req: req,
resp: resp,
span: span,
logger: logger,
t: t,
}
} else {
t.logAndSpanError(req, resp, &googleapi.Error{
Code: resp.StatusCode,
Message: resp.Status,
}, nil, span, logger)
}
} else {
if span != nil {
span.SetAttributes(attribute.Int("http.response.status_code", resp.StatusCode))
}
}
return resp, err
}
func (t *otelAttributeTransport) logAndSpanError(req *http.Request, resp *http.Response, errToParse error, netErr error, span trace.Span, logger *slog.Logger) {
var httpStatusCode int
if resp != nil {
httpStatusCode = resp.StatusCode
}
info := gax.ExtractTelemetryErrorInfo(req.Context(), errToParse)
if netErr == nil && resp != nil && resp.StatusCode >= 400 {
if info.ErrorType == "*googleapi.Error" {
info.ErrorType = strconv.Itoa(resp.StatusCode)
}
}
if logger != nil {
logAttrs := []slog.Attr{
slog.String("rpc.system.name", "http"),
}
if httpStatusCode > 0 {
logAttrs = append(logAttrs, slog.Int64("http.response.status_code", int64(httpStatusCode)))
}
ctx := req.Context()
if resendCountStr, ok := callctx.TelemetryFromContext(ctx, "resend_count"); ok {
if count, e := strconv.Atoi(resendCountStr); e == nil {
logAttrs = append(logAttrs, slog.Int64("http.request.resend_count", int64(count)))
}
}
if urlTemplate, ok := callctx.TelemetryFromContext(ctx, "url_template"); ok {
logAttrs = append(logAttrs, slog.String("url.template", urlTemplate))
}
logAttrs = append(logAttrs, slog.String("http.request.method", req.Method))
msg := info.StatusMessage
if msg == "" {
msg = "API call failed"
}
if rpcMethod, ok := callctx.TelemetryFromContext(ctx, "rpc_method"); ok {
logAttrs = append(logAttrs, slog.String("rpc.method", rpcMethod))
}
if resName, ok := callctx.TelemetryFromContext(ctx, "resource_name"); ok {
logAttrs = append(logAttrs, slog.String("gcp.resource.destination.id", resName))
}
if info.Domain != "" {
logAttrs = append(logAttrs, slog.String("gcp.errors.domain", info.Domain))
}
for k, v := range info.Metadata {
logAttrs = append(logAttrs, slog.String("gcp.errors.metadata."+k, v))
}
logAttrs = append(logAttrs, slog.String("error.type", info.ErrorType))
if info.StatusCode != "" {
logAttrs = append(logAttrs, slog.String("rpc.response.status_code", info.StatusCode))
}
logger.LogAttrs(ctx, slog.LevelDebug, msg, logAttrs...)
}
if span != nil {
if netErr != nil {
span.SetAttributes(
attribute.String("error.type", info.ErrorType),
attribute.String("status.message", info.StatusMessage),
attribute.String("exception.type", fmt.Sprintf("%T", netErr)),
)
} else {
span.SetAttributes(
attribute.Int("http.response.status_code", httpStatusCode),
attribute.String("error.type", info.ErrorType),
attribute.String("status.message", info.StatusMessage),
)
}
}
}
type errorTrackingBody struct {
io.ReadCloser
req *http.Request
resp *http.Response
span trace.Span
logger *slog.Logger
t *otelAttributeTransport
mu sync.Mutex
buf bytes.Buffer
recorded bool
}
func (b *errorTrackingBody) Read(p []byte) (n int, err error) {
n, err = b.ReadCloser.Read(p)
b.mu.Lock()
shouldRecord := false
if !b.recorded {
if n > 0 {
remaining := maxErrorReadBytes - int64(b.buf.Len())
if remaining > 0 {
if int64(n) > remaining {
b.buf.Write(p[:remaining])
} else {
b.buf.Write(p[:n])
}
}
}
if err == io.EOF || int64(b.buf.Len()) >= maxErrorReadBytes {
shouldRecord = true
b.recorded = true
}
}
b.mu.Unlock()
if shouldRecord {
b.recordError()
}
return n, err
}
func (b *errorTrackingBody) Close() error {
b.mu.Lock()
shouldRecord := !b.recorded
b.recorded = true
b.mu.Unlock()
// We can close the network stream immediately.
err := b.ReadCloser.Close()
// Do heavy in-memory telemetry parsing without holding the lock
if shouldRecord {
b.recordError() // If this panics here, the socket is already released
}
return err
}
func (b *errorTrackingBody) recordError() {
errToParse := &googleapi.Error{
Code: b.resp.StatusCode,
Message: b.resp.Status,
}
if b.buf.Len() > 0 {
clone := *b.resp
clone.Body = io.NopCloser(bytes.NewReader(b.buf.Bytes()))
if errResp := googleapi.CheckResponse(&clone); errResp != nil {
if gErr, ok := errResp.(*googleapi.Error); ok {
if gErr.Message == "" {
gErr.Message = b.resp.Status
}
errToParse = gErr
} else {
errToParse = &googleapi.Error{
Code: b.resp.StatusCode,
Message: errResp.Error(),
}
}
}
}
b.t.logAndSpanError(b.req, b.resp, errToParse, nil, b.span, b.logger)
}
type authTransport struct {
creds *auth.Credentials
base http.RoundTripper
+2 -2
View File
@@ -4,7 +4,7 @@
// 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
// https://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,
@@ -17,4 +17,4 @@
package internal
// Version is the current tagged release of the library.
const Version = "0.19.0"
const Version = "0.20.0"
+13
View File
@@ -1,5 +1,18 @@
# Release History
## 1.21.1 (2026-04-16)
### Bugs Fixed
* Fixed an issue in `ResponseError.Error()` where the request URL path was being logged unescaped.
* Redact query parameters when logging errors.
* For `runtime.JoinPaths`, don't add a slash between root and paths when `paths` starts with `?` (query string).
### Other Changes
* Upgraded to Go 1.25.0.
* Upgraded dependencies.
## 1.21.0 (2026-01-12)
### Features Added
@@ -131,7 +131,7 @@ func (e *ResponseError) Error() string {
msg := &bytes.Buffer{}
if e.RawResponse != nil {
if e.RawResponse.Request != nil {
fmt.Fprintf(msg, "%s %s://%s%s\n", e.RawResponse.Request.Method, e.RawResponse.Request.URL.Scheme, e.RawResponse.Request.URL.Host, e.RawResponse.Request.URL.Path)
fmt.Fprintf(msg, "%s %s://%s%s\n", e.RawResponse.Request.Method, e.RawResponse.Request.URL.Scheme, e.RawResponse.Request.URL.Host, e.RawResponse.Request.URL.EscapedPath())
} else {
fmt.Fprintln(msg, "Request information not available")
}
@@ -37,5 +37,5 @@ const (
Module = "azcore"
// Version is the semantic version (see http://semver.org) of this module.
Version = "v1.21.0"
Version = "v1.21.1"
)
@@ -5,10 +5,8 @@ package runtime
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
@@ -73,14 +71,7 @@ func (h *httpTracePolicy) Do(req *policy.Request) (resp *http.Response, err erro
span.SetAttributes(tracing.Attribute{Key: attrAZServiceReqID, Value: reqID})
}
} else if err != nil {
var urlErr *url.Error
if errors.As(err, &urlErr) {
// calling *url.Error.Error() will include the unsanitized URL
// which we don't want. in addition, we already have the HTTP verb
// and sanitized URL in the trace so we aren't losing any info
err = urlErr.Err
}
span.SetStatus(tracing.SpanStatusError, err.Error())
span.SetStatus(tracing.SpanStatusError, getSanitizedURLString(err.Error(), req.Raw().URL, h.allowedQP))
}
span.End()
}()
@@ -158,6 +158,27 @@ func getSanitizedURL(u url.URL, allowedQueryParams map[string]struct{}) string {
return u.String()
}
// getSanitizedURLString returns s with the query params of u redacted.
// if s doesn't contain u, then s is returned unchanged.
func getSanitizedURLString(s string, u *url.URL, allowedQueryParams map[string]struct{}) string {
if u.RawQuery == "" {
// the URL doesn't have any query params, so nothing to redact
return s
}
urlIndex := strings.Index(strings.ToLower(s), strings.ToLower(u.String()))
if urlIndex < 0 {
// the URL isn't in the string, so nothing to redact
return s
}
// replace the unsanitized URL in the error message with the sanitized version
sanitizedURL := getSanitizedURL(*u, allowedQueryParams)
s = s[:urlIndex] + sanitizedURL + s[urlIndex+len(u.String()):]
return s
}
// writeRequestWithResponse appends a formatted HTTP request into a Buffer. If request and/or err are
// not nil, then these are also written into the Buffer.
func (p *logPolicy) writeRequestWithResponse(b *bytes.Buffer, req *policy.Request, resp *http.Response, err error) {
@@ -171,7 +192,7 @@ func (p *logPolicy) writeRequestWithResponse(b *bytes.Buffer, req *policy.Reques
}
if err != nil {
fmt.Fprintln(b, " --------------------------------------------------------------------------------")
fmt.Fprint(b, " ERROR:\n"+err.Error()+"\n")
fmt.Fprint(b, " ERROR:\n"+getSanitizedURLString(err.Error(), req.Raw().URL, p.allowedQP)+"\n")
}
}
+6 -2
View File
@@ -90,12 +90,16 @@ func JoinPaths(root string, paths ...string) string {
}
if qps != "" {
p = p + "?" + qps
if strings.Contains(p, "?") {
p = p + "&" + qps
} else {
p = p + "?" + qps
}
}
if strings.HasSuffix(root, "/") && strings.HasPrefix(p, "/") {
root = root[:len(root)-1]
} else if !strings.HasSuffix(root, "/") && !strings.HasPrefix(p, "/") {
} else if !strings.HasSuffix(root, "/") && !strings.HasPrefix(p, "/") && !strings.HasPrefix(p, "?") {
p = "/" + p
}
return root + p
+1 -4
View File
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
@@ -42,7 +39,7 @@ func StackTrace(skipFrames, totalFrames int) string {
sb.WriteString("()\n\t")
sb.WriteString(frame.File)
sb.WriteRune(':')
sb.WriteString(fmt.Sprintf("%d\n", frame.Line))
fmt.Fprintf(&sb, "%d\n", frame.Line)
if !more {
break
}
-3
View File
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+3 -10
View File
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
@@ -10,6 +7,7 @@ import (
"errors"
"io"
"net/http"
"slices"
)
// HasStatusCode returns true if the Response's status code is one of the specified values.
@@ -18,12 +16,7 @@ func HasStatusCode(resp *http.Response, statusCodes ...int) bool {
if resp == nil {
return false
}
for _, sc := range statusCodes {
if resp.StatusCode == sc {
return true
}
}
return false
return slices.Contains(statusCodes, resp.StatusCode)
}
// PayloadOptions contains the optional values for the Payload func.
@@ -57,7 +50,7 @@ func Payload(resp *http.Response, opts *PayloadOptions) ([]byte, error) {
}
bytesBody, err := io.ReadAll(resp.Body)
resp.Body.Close()
_ = resp.Body.Close()
if err != nil {
return nil, err
}
-3
View File
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-3
View File
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-3
View File
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
@@ -93,7 +90,7 @@ func (er *Resource[TResource, TState]) Get(state TState) (TResource, error) {
}
// Getting here means that this thread/goroutine will wait for the updated resource
} else if er.shouldRefresh(resource, state) {
if !(er.acquiring || backoff(now, er.lastAttempt)) {
if !er.acquiring && !backoff(now, er.lastAttempt) {
// If another thread/goroutine is not acquiring/renewing the resource, and none has attempted
// to do so within the last 30 seconds, this thread/goroutine will do it
er.acquiring, acquire = true, true
-3
View File
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-3
View File
@@ -1,6 +1,3 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+4
View File
@@ -1,3 +1,7 @@
codecov:
notify:
after_n_builds: 2
coverage:
status:
patch:
+2 -1
View File
@@ -3,4 +3,5 @@
.idea
.env
.mcp.json
.claude/
go.work.sum
.worktrees
+1
View File
@@ -14,6 +14,7 @@ linters:
- recvcheck
- testpackage
- thelper
- tagliatelle
- tparallel
- varnamelen
- whitespace
+19 -12
View File
@@ -4,24 +4,31 @@
| Total Contributors | Total Contributions |
| --- | --- |
| 15 | 202 |
| 22 | 267 |
| Username | All Time Contribution Count | All Commits |
| --- | --- | --- |
| @fredbi | 99 | <https://github.com/go-openapi/analysis/commits?author=fredbi> |
| @casualjim | 70 | <https://github.com/go-openapi/analysis/commits?author=casualjim> |
| @fredbi | 127 | <https://github.com/go-openapi/analysis/commits?author=fredbi> |
| @casualjim | 91 | <https://github.com/go-openapi/analysis/commits?author=casualjim> |
| @keramix | 9 | <https://github.com/go-openapi/analysis/commits?author=keramix> |
| @youyuanwu | 8 | <https://github.com/go-openapi/analysis/commits?author=youyuanwu> |
| @msample | 3 | <https://github.com/go-openapi/analysis/commits?author=msample> |
| @wjase | 7 | <https://github.com/go-openapi/analysis/commits?author=wjase> |
| @kul-amr | 3 | <https://github.com/go-openapi/analysis/commits?author=kul-amr> |
| @schafle | 3 | <https://github.com/go-openapi/analysis/commits?author=schafle> |
| @msample | 3 | <https://github.com/go-openapi/analysis/commits?author=msample> |
| @mbohlool | 2 | <https://github.com/go-openapi/analysis/commits?author=mbohlool> |
| @Copilot | 1 | <https://github.com/go-openapi/analysis/commits?author=Copilot> |
| @danielfbm | 1 | <https://github.com/go-openapi/analysis/commits?author=danielfbm> |
| @gregmarr | 1 | <https://github.com/go-openapi/analysis/commits?author=gregmarr> |
| @guillemj | 1 | <https://github.com/go-openapi/analysis/commits?author=guillemj> |
| @knweiss | 1 | <https://github.com/go-openapi/analysis/commits?author=knweiss> |
| @tklauser | 1 | <https://github.com/go-openapi/analysis/commits?author=tklauser> |
| @cuishuang | 1 | <https://github.com/go-openapi/analysis/commits?author=cuishuang> |
| @zmay2030 | 2 | <https://github.com/go-openapi/analysis/commits?author=zmay2030> |
| @ujjwalsh | 1 | <https://github.com/go-openapi/analysis/commits?author=ujjwalsh> |
| @itengfei | 1 | <https://github.com/go-openapi/analysis/commits?author=itengfei> |
| @nrnrk | 1 | <https://github.com/go-openapi/analysis/commits?author=nrnrk> |
| @cuishuang | 1 | <https://github.com/go-openapi/analysis/commits?author=cuishuang> |
| @tklauser | 1 | <https://github.com/go-openapi/analysis/commits?author=tklauser> |
| @Shimizu1111 | 1 | <https://github.com/go-openapi/analysis/commits?author=Shimizu1111> |
| @thaJeztah | 1 | <https://github.com/go-openapi/analysis/commits?author=thaJeztah> |
| @knweiss | 1 | <https://github.com/go-openapi/analysis/commits?author=knweiss> |
| @guillemj | 1 | <https://github.com/go-openapi/analysis/commits?author=guillemj> |
| @gregmarr | 1 | <https://github.com/go-openapi/analysis/commits?author=gregmarr> |
| @danielfbm | 1 | <https://github.com/go-openapi/analysis/commits?author=danielfbm> |
| @Copilot | 1 | <https://github.com/go-openapi/analysis/commits?author=Copilot> |
_this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_
_this file was generated by the [Contributors GitHub Action](https://github.com/github-community-projects/contributors)_
+3 -2
View File
@@ -12,7 +12,7 @@
---
A foundational library to analyze an OAI specification document for easier reasoning about the content.
A foundational library to analyze, diff, flatten, merge, and fix OAI specification documents for easier reasoning about the content.
## Announcements
@@ -38,6 +38,7 @@ go get github.com/go-openapi/analysis
* An analyzer providing methods to walk the functional content of a specification
* A spec flattener producing a self-contained document bundle, while preserving `$ref`s
* A spec differ ("diff") to compare two specs and report structural and compatibility changes
* A spec merger ("mixin") to merge several spec documents into a primary spec
* A spec "fixer" ensuring that response descriptions are non empty
@@ -115,7 +116,7 @@ Maintainers can cut a new release by either:
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
[discord-url]: https://discord.gg/twZ9BwT3
[discord-url]: https://discord.gg/FfnFYaC3k5
<!-- Badges: license & compliance -->
[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
+24 -17
View File
@@ -145,19 +145,27 @@ type Spec struct {
enums enumAnalysis
allSchemas map[string]SchemaRef
allOfs map[string]SchemaRef
mangler mangling.NameMangler
}
// New takes a swagger spec object and returns an analyzed spec document.
// The analyzed document contains a number of indices that make it easier to
// reason about semantics of a swagger specification for use in code generation
// or validation etc.
func New(doc *spec.Swagger) *Spec {
func New(doc *spec.Swagger, opts ...Option) *Spec {
o := &analyzerOptions{}
for _, opt := range opts {
opt(o)
}
a := &Spec{
spec: doc,
references: referenceAnalysis{},
patterns: patternAnalysis{},
enums: enumAnalysis{},
mangler: mangling.NewNameMangler(o.manglerOpts...),
}
a.reset()
a.initialize()
@@ -288,20 +296,6 @@ func (s *Spec) ProducesFor(operation *spec.Operation) []string {
return s.structMapKeys(prod)
}
func mapKeyFromParam(param *spec.Parameter) string {
return fmt.Sprintf("%s#%s", param.In, fieldNameFromParam(param))
}
func fieldNameFromParam(param *spec.Parameter) string {
// TODO: this should be x-go-name
if nm, ok := param.Extensions.GetString("go-name"); ok {
return nm
}
mangler := mangling.NewNameMangler()
return mangler.ToGoName(param.Name)
}
// ErrorOnParamFunc is a callback function to be invoked
// whenever an error is encountered while resolving references
// on parameters.
@@ -651,6 +645,19 @@ func (s *Spec) AllEnums() map[string][]any {
return cloneEnumMap(s.enums.allEnums)
}
func (s *Spec) mapKeyFromParam(param *spec.Parameter) string {
return fmt.Sprintf("%s#%s", param.In, s.fieldNameFromParam(param))
}
func (s *Spec) fieldNameFromParam(param *spec.Parameter) string {
// TODO: this should be x-go-name
if nm, ok := param.Extensions.GetString("go-name"); ok {
return nm
}
return s.mangler.ToGoName(param.Name)
}
func (s *Spec) structMapKeys(mp map[string]struct{}) []string {
if len(mp) == 0 {
return nil
@@ -668,7 +675,7 @@ func (s *Spec) paramsAsMap(parameters []spec.Parameter, res map[string]spec.Para
for _, param := range parameters {
pr := param
if pr.Ref.String() == "" {
res[mapKeyFromParam(&pr)] = pr
res[s.mapKeyFromParam(&pr)] = pr
continue
}
@@ -699,7 +706,7 @@ func (s *Spec) paramsAsMap(parameters []spec.Parameter, res map[string]spec.Para
}
pr = objAsParam
res[mapKeyFromParam(&pr)] = pr
res[s.mapKeyFromParam(&pr)] = pr
}
}
+1
View File
@@ -554,6 +554,7 @@ func updateRefParents(allRefs map[string]spec.Ref, r *newRef) {
}
}
//nolint:gocognit,gocyclo,cyclop // legacy from a lot of design choices that led to concentrate the complexity just here.
func stripOAIGenForRef(opts *FlattenOpts, k string, r *newRef) (bool, error) {
replacedWithComplex := false
+2 -2
View File
@@ -273,9 +273,9 @@ func mangler(o *FlattenOpts) func(string) string {
if o.KeepNames {
return func(in string) string { return in }
}
mangler := mangling.NewNameMangler()
m := mangling.NewNameMangler(o.ManglerOpts...)
return mangler.ToJSONName
return m.ToJSONName
}
func nameFromRef(ref spec.Ref, o *FlattenOpts) string {
+8 -6
View File
@@ -7,6 +7,7 @@ import (
"log"
"github.com/go-openapi/spec"
"github.com/go-openapi/swag/mangling"
)
// FlattenOpts configuration for flattening a swagger specification.
@@ -24,12 +25,13 @@ type FlattenOpts struct {
BasePath string // The location of the root document for this spec to resolve relative $ref
// Flattening options
Expand bool // When true, skip flattening the spec and expand it instead (if Minimal is false)
Minimal bool // When true, do not decompose complex structures such as allOf
Verbose bool // enable some reporting on possible name conflicts detected
RemoveUnused bool // When true, remove unused parameters, responses and definitions after expansion/flattening
ContinueOnError bool // Continue when spec expansion issues are found
KeepNames bool // Do not attempt to jsonify names from references when flattening
Expand bool // When true, skip flattening the spec and expand it instead (if Minimal is false)
Minimal bool // When true, do not decompose complex structures such as allOf
Verbose bool // enable some reporting on possible name conflicts detected
RemoveUnused bool // When true, remove unused parameters, responses and definitions after expansion/flattening
ContinueOnError bool // Continue when spec expansion issues are found
KeepNames bool // Do not attempt to jsonify names from references when flattening
ManglerOpts []mangling.Option // Options for the name mangler used to jsonify names
/* Extra keys */
_ struct{} // require keys
+1 -1
View File
@@ -1,4 +1,4 @@
go 1.24.0
go 1.25.0
use (
.
-29
View File
@@ -1,29 +0,0 @@
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
+50 -21
View File
@@ -11,37 +11,66 @@ import (
"github.com/go-openapi/spec"
)
// Mixin modifies the primary swagger spec by adding the paths and
// definitions from the mixin specs. Top level parameters and
// responses from the mixins are also carried over. Operation id
// collisions are avoided by appending "Mixin<N>" but only if
// needed.
// Mixin merges one or more Swagger 2.0 documents into a primary document.
//
// The following parts of primary are subject to merge, filling empty details
// # Argument order and precedence
//
// - Info
// The first argument is the primary spec, which Mixin modifies in place.
// Subsequent arguments are mixins, listed in decreasing order of priority.
// On any collision, the primary always wins; among mixins, the earliest one
// wins.
//
// Example: given a primary spec with host "a.example.com" and a mixin with
// host "b.example.com", the merged result keeps "a.example.com" (primary
// wins, the mixin value is dropped). Given a primary without a host and a
// mixin with host "b.example.com", the merged result uses "b.example.com"
// (the mixin fills in the empty field on the primary).
//
// # What gets merged
//
// Top-level scalar fields on the primary are filled from the first mixin
// that provides them, but only if the primary's value is the zero value:
//
// - Info (including the nested Contact and License)
// - BasePath
// - Host
// - ExternalDocs
//
// Consider calling [FixEmptyResponseDescriptions]() on the modified primary
// if you read them from storage and they are valid to start with.
// Map and slice fields are merged entry by entry. This covers:
//
// Entries in "paths", "definitions", "parameters" and "responses" are
// added to the primary in the order of the given mixins. If the entry
// already exists in primary it is skipped with a warning message.
// - paths, definitions, parameters, responses
// - securityDefinitions, security, tags
// - top-level and Info extensions
//
// The count of skipped entries (from collisions) is returned so any
// deviation from the number expected can flag a warning in your build
// scripts. Carefully review the collisions before accepting them;
// consider renaming things if possible.
// Duplicate keys (or equal security requirements, or equal tag names) are
// skipped with a warning; warnings are returned as a slice and intended to
// be inspected by the caller (e.g. compared to an expected collision count
// in build scripts).
//
// No key normalization takes place (paths, type defs,
// etc). Ensure they are canonical if your downstream tools do
// key normalization of any form.
// Schemes, consumes and produces are merged as the union of distinct
// values. Duplicates there are silently dropped, no warning is emitted.
//
// Merging schemes ([http], https), and consumers/producers do not account for
// collisions.
// Operation id collisions are auto-resolved by appending "Mixin<N>" to the
// mixin operation id (N is the mixin index), so the merged spec keeps
// unique operation ids.
//
// # Notes and limitations
//
// Consider calling [FixEmptyResponseDescriptions] on the modified primary
// if you read responses from storage and they are valid to start with.
//
// No key normalization takes place. Ensure paths, type names, etc. are
// canonical if your downstream tools rely on normalized forms.
//
// YAML anchors (& / *) are resolved by the YAML parser before Mixin sees
// the document, so they are not preserved in the merged output, and they
// cannot be shared across input files. Use $ref for cross-file reuse. See
// https://goswagger.io/go-swagger/faq/faq_swagger/#does-swagger-mixin-preserve-yaml-anchors
//
// The order of paths and definitions in the merged output is alphabetical:
// the underlying spec model stores them as Go maps, which serialize with
// sorted keys. Source-file order is not preserved. See
// https://goswagger.io/go-swagger/faq/faq_swagger/#can-i-control-the-path-or-operation-order-in-swagger-mixin-output
func Mixin(primary *spec.Swagger, mixins ...*spec.Swagger) []string {
skipped := make([]string, 0, len(mixins))
opIDs := getOpIDs(primary)
+21
View File
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package analysis
import "github.com/go-openapi/swag/mangling"
// Option configures the behavior of a new [Spec] analyzer.
type Option func(*analyzerOptions)
type analyzerOptions struct {
manglerOpts []mangling.Option
}
// WithManglerOptions sets the name mangler options used when building
// Go identifiers from specification names (e.g. parameter names).
func WithManglerOptions(opts ...mangling.Option) Option {
return func(o *analyzerOptions) {
o.manglerOpts = append(o.manglerOpts, opts...)
}
}
-181
View File
@@ -1,181 +0,0 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[changelog]
header = """
"""
footer = """
-----
**[{{ remote.github.repo }}]({{ self::remote_url() }}) license terms**
[![License][license-badge]][license-url]
[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
[license-url]: {{ self::remote_url() }}/?tab=Apache-2.0-1-ov-file#readme
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
"""
body = """
{%- if version %}
## [{{ version | trim_start_matches(pat="v") }}]({{ self::remote_url() }}/tree/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{%- else %}
## [unreleased]
{%- endif %}
{%- if message %}
{%- raw %}\n{% endraw %}
{{ message }}
{%- raw %}\n{% endraw %}
{%- endif %}
{%- if version %}
{%- if previous.version %}
**Full Changelog**: <{{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}>
{%- endif %}
{%- else %}
{%- raw %}\n{% endraw %}
{%- endif %}
{%- if statistics %}{% if statistics.commit_count %}
{%- raw %}\n{% endraw %}
{{ statistics.commit_count }} commits in this release.
{%- raw %}\n{% endraw %}
{%- endif %}{% endif %}
-----
{%- for group, commits in commits | group_by(attribute="group") %}
{%- raw %}\n{% endraw %}
### {{ group | upper_first }}
{%- raw %}\n{% endraw %}
{%- for commit in commits %}
{%- if commit.remote.pr_title %}
{%- set commit_message = commit.remote.pr_title %}
{%- else %}
{%- set commit_message = commit.message %}
{%- endif %}
* {{ commit_message | split(pat="\n") | first | trim }}
{%- if commit.remote.username %}
{%- raw %} {% endraw %}by [@{{ commit.remote.username }}](https://github.com/{{ commit.remote.username }})
{%- endif %}
{%- if commit.remote.pr_number %}
{%- raw %} {% endraw %}in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }})
{%- endif %}
{%- raw %} {% endraw %}[...]({{ self::remote_url() }}/commit/{{ commit.id }})
{%- endfor %}
{%- endfor %}
{%- if github %}
{%- raw %}\n{% endraw -%}
{%- set all_contributors = github.contributors | length %}
{%- if github.contributors | filter(attribute="username", value="dependabot[bot]") | length < all_contributors %}
-----
### People who contributed to this release
{% endif %}
{%- for contributor in github.contributors | filter(attribute="username") | sort(attribute="username") %}
{%- if contributor.username != "dependabot[bot]" and contributor.username != "github-actions[bot]" %}
* [@{{ contributor.username }}](https://github.com/{{ contributor.username }})
{%- endif %}
{%- endfor %}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
-----
{%- raw %}\n{% endraw %}
### New Contributors
{%- endif %}
{%- for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
{%- if contributor.username != "dependabot[bot]" and contributor.username != "github-actions[bot]" %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %}
in [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endif %}
{%- endfor %}
{%- endif %}
{%- raw %}\n{% endraw %}
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
"""
# Remove leading and trailing whitespaces from the changelog's body.
trim = true
# Render body even when there are no releases to process.
render_always = true
# An array of regex based postprocessors to modify the changelog.
postprocessors = [
# Replace the placeholder <REPO> with a URL.
#{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
]
# output file path
# output = "test.md"
[git]
# Parse commits according to the conventional commits specification.
# See https://www.conventionalcommits.org
conventional_commits = false
# Exclude commits that do not match the conventional commits specification.
filter_unconventional = false
# Require all commits to be conventional.
# Takes precedence over filter_unconventional.
require_conventional = false
# Split commits on newlines, treating each line as an individual commit.
split_commits = false
# An array of regex based parsers to modify commit messages prior to further processing.
commit_preprocessors = [
# Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit message using https://github.com/crate-ci/typos.
# If the spelling is incorrect, it will be fixed automatically.
#{ pattern = '.*', replace_command = 'typos --write-changes -' }
]
# Prevent commits that are breaking from being excluded by commit parsers.
protect_breaking_commits = false
# An array of regex based parsers for extracting data from the commit message.
# Assigns commits to groups.
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
commit_parsers = [
{ message = "^[Cc]hore\\([Rr]elease\\): prepare for", skip = true },
{ message = "(^[Mm]erge)|([Mm]erge conflict)", skip = true },
{ field = "author.name", pattern = "dependabot*", group = "<!-- 0A -->Updates" },
{ message = "([Ss]ecurity)|([Vv]uln)", group = "<!-- 08 -->Security" },
{ body = "(.*[Ss]ecurity)|([Vv]uln)", group = "<!-- 08 -->Security" },
{ message = "([Cc]hore\\(lint\\))|(style)|(lint)|(codeql)|(golangci)", group = "<!-- 05 -->Code quality" },
{ message = "(^[Dd]oc)|((?i)readme)|(badge)|(typo)|(documentation)", group = "<!-- 03 -->Documentation" },
{ message = "(^[Ff]eat)|(^[Ee]nhancement)", group = "<!-- 00 -->Implemented enhancements" },
{ message = "(^ci)|(\\(ci\\))|(fixup\\s+ci)|(fix\\s+ci)|(license)|(example)", group = "<!-- 07 -->Miscellaneous tasks" },
{ message = "^test", group = "<!-- 06 -->Testing" },
{ message = "(^fix)|(panic)", group = "<!-- 01 -->Fixed bugs" },
{ message = "(^refact)|(rework)", group = "<!-- 02 -->Refactor" },
{ message = "(^[Pp]erf)|(performance)", group = "<!-- 04 -->Performance" },
{ message = "(^[Cc]hore)", group = "<!-- 07 -->Miscellaneous tasks" },
{ message = "^[Rr]evert", group = "<!-- 09 -->Reverted changes" },
{ message = "(upgrade.*?go)|(go\\s+version)", group = "<!-- 0A -->Updates" },
{ message = ".*", group = "<!-- 0B -->Other" },
]
# Exclude commits that are not matched by any commit parser.
filter_commits = false
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
link_parsers = []
# Include only the tags that belong to the current branch.
use_branch_tags = false
# Order releases topologically instead of chronologically.
topo_order = false
# Order releases topologically instead of chronologically.
topo_order_commits = true
# Order of commits in each group/release within the changelog.
# Allowed values: newest, oldest
sort_commits = "newest"
# Process submodules commits
recurse_submodules = false
#[remote.github]
#owner = "go-openapi"
-1
View File
@@ -3,4 +3,3 @@
.idea
.env
.mcp.json
.claude/
+4 -3
View File
@@ -4,11 +4,11 @@
| Total Contributors | Total Contributions |
| --- | --- |
| 12 | 101 |
| 13 | 111 |
| Username | All Time Contribution Count | All Commits |
| --- | --- | --- |
| @fredbi | 54 | <https://github.com/go-openapi/jsonpointer/commits?author=fredbi> |
| @fredbi | 63 | <https://github.com/go-openapi/jsonpointer/commits?author=fredbi> |
| @casualjim | 33 | <https://github.com/go-openapi/jsonpointer/commits?author=casualjim> |
| @magodo | 3 | <https://github.com/go-openapi/jsonpointer/commits?author=magodo> |
| @youyuanwu | 3 | <https://github.com/go-openapi/jsonpointer/commits?author=youyuanwu> |
@@ -18,7 +18,8 @@
| @ianlancetaylor | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=ianlancetaylor> |
| @mfleader | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=mfleader> |
| @Neo2308 | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=Neo2308> |
| @alexandear | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=alexandear> |
| @olivierlemasle | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=olivierlemasle> |
| @testwill | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=testwill> |
_this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_
_this file was generated by the [Contributors GitHub Action](https://github.com/github-community-projects/contributors)_
+1 -1
View File
@@ -18,7 +18,7 @@ It ships with copies of other software which license terms are recalled below.
The original software was authored on 25-02-2013 by sigu-399 (https://github.com/sigu-399, sigu.399@gmail.com).
github.com/sigh-399/jsonpointer
github.com/sigu-399/jsonpointer
===========================
// SPDX-FileCopyrightText: Copyright 2013 sigu-399 ( https://github.com/sigu-399 )
+31 -21
View File
@@ -16,17 +16,25 @@ An implementation of JSON Pointer for golang, which supports go `struct`.
## Announcements
* **2025-12-19** : new community chat on discord
* a new discord community channel is available to be notified of changes and support users
* our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31**
* **2026-04-15** : added support for trailing "-" for arrays (v0.23.0)
* this brings full support of [RFC6901][RFC6901]
* this is supported for types relying on the reflection-based implemented
* API semantics remain essentially unaltered. Exception: `Pointer.Set(document any,value any) (document any, err error)`
can only perform a best-effort to mutate the input document in place. In the case of adding elements to an array with a
trailing "-", either pass a mutable array (`*[]T`) as the input document, or use the returned updated document instead.
* types that implement the `JSONSetable` interface may not implement the mutation implied by the trailing "-"
You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url]
Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url]
* **2026-04-15** : added support for optional alternate JSON name providers
* for struct support the defaults might not suit all situations: there are known limitations
when it comes to handle untagged fields or embedded types.
* the default name provider in use is not fully aligned with go JSON stdlib
* exposed an option (or global setting) to change the provider that resolves a struct into json keys
* the default behavior is not altered
* a new alternate name provider is added (imported from `go-openapi/swag/jsonname`), aligned with JSON stdlib behavior
## Status
API is stable.
API is stable and feature-complete.
## Import this library in your project
@@ -88,7 +96,7 @@ See <https://github.com/go-openapi/jsonpointer/releases>
<https://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07>
also known as [RFC6901](https://www.rfc-editor.org/rfc/rfc6901)
also known as [RFC6901][RFC6901].
## Licensing
@@ -99,19 +107,19 @@ on top of which it has been built.
## Limitations
The 4.Evaluation part of the previous reference, starting with 'If the currently referenced value is a JSON array,
the reference token MUST contain either...' is not implemented.
That is because our implementation of the JSON pointer only supports explicit references to array elements:
the provision in the spec to resolve non-existent members as "the last element in the array",
using the special trailing character "-" is not implemented.
* [RFC6901][RFC6901] is now fully supported, including trailing "-" semantics for arrays (for `Set` operations).
* Default behavior: JSON name detection in go `struct`s
- Unlike go standard marshaling, untagged fields do not default to the go field name and are ignored.
- anonymous fields are not traversed if untagged
- the above limitations may be overcome by calling `UseGoNameProvider()` at initialization time.
- alternatively, users may inject the desired custom behavior for naming fields as an option.
## Other documentation
* [All-time contributors](./CONTRIBUTORS.md)
* [Contributing guidelines](.github/CONTRIBUTING.md)
* [Maintainers documentation](docs/MAINTAINERS.md)
* [Code style](docs/STYLE.md)
* [Contributing guidelines][contributing-doc-site]
* [Maintainers documentation][maintainers-doc-site]
* [Code style][style-doc-site]
## Cutting a new release
@@ -142,11 +150,8 @@ Maintainers can cut a new release by either:
<!-- Badges: documentation & support -->
[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/jsonpointer
[godoc-url]: http://pkg.go.dev/github.com/go-openapi/jsonpointer
[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
[discord-url]: https://discord.gg/twZ9BwT3
[discord-url]: https://discord.gg/FfnFYaC3k5
<!-- Badges: license & compliance -->
[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
@@ -156,3 +161,8 @@ Maintainers can cut a new release by either:
[goversion-url]: https://github.com/go-openapi/jsonpointer/blob/master/go.mod
[top-badge]: https://img.shields.io/github/languages/top/go-openapi/jsonpointer
[commits-badge]: https://img.shields.io/github/commits-since/go-openapi/jsonpointer/latest
[RFC6901]: https://www.rfc-editor.org/rfc/rfc6901
<!-- Organization docs -->
[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
+25 -1
View File
@@ -16,12 +16,24 @@ const (
ErrPointer pointerError = "JSON pointer error"
// ErrInvalidStart states that a JSON pointer must start with a separator ("/").
ErrInvalidStart pointerError = `JSON pointer must be empty or start with a "` + pointerSeparator
ErrInvalidStart pointerError = `JSON pointer must be empty or start with a "` + pointerSeparator + `"`
// ErrUnsupportedValueType indicates that a value of the wrong type is being set.
ErrUnsupportedValueType pointerError = "only structs, pointers, maps and slices are supported for setting values"
// ErrDashToken indicates use of the RFC 6901 "-" reference token
// in a context where it cannot be resolved.
//
// Per RFC 6901 §4 the "-" token refers to the (nonexistent) element
// after the last array element. It may only be used as the terminal
// token of a [Pointer.Set] against a slice, where it means "append".
// Any other use (get, offset, intermediate traversal, non-slice target)
// is an error condition that wraps this sentinel.
ErrDashToken pointerError = `the "-" array token cannot be resolved here` //nolint:gosec // G101 false positive: this is a JSON Pointer reference token, not a credential.
)
const dashToken = "-"
func errNoKey(key string) error {
return fmt.Errorf("object has no key %q: %w", key, ErrPointer)
}
@@ -33,3 +45,15 @@ func errOutOfBounds(length, idx int) error {
func errInvalidReference(token string) error {
return fmt.Errorf("invalid token reference %q: %w", token, ErrPointer)
}
func errDashOnGet() error {
return fmt.Errorf("cannot resolve %q token on get: %w: %w", dashToken, ErrDashToken, ErrPointer)
}
func errDashIntermediate() error {
return fmt.Errorf("the %q token may only appear as the terminal token of a pointer: %w: %w", dashToken, ErrDashToken, ErrPointer)
}
func errDashOnOffset() error {
return fmt.Errorf("cannot compute offset for %q token (nonexistent element): %w: %w", dashToken, ErrDashToken, ErrPointer)
}
+47
View File
@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package jsonpointer
import "reflect"
// JSONPointable is an interface for structs to implement,
// when they need to customize the json pointer process or want to avoid the use of reflection.
type JSONPointable interface {
// JSONLookup returns a value pointed at this (unescaped) key.
JSONLookup(key string) (any, error)
}
// JSONSetable is an interface for structs to implement,
// when they need to customize the json pointer process or want to avoid the use of reflection.
//
// # Handling of the RFC 6901 "-" token
//
// When a type implementing JSONSetable is the terminal parent of a [Pointer.Set]
// call, the library passes the raw reference token to JSONSet without
// interpretation. In particular, the RFC 6901 "-" token (which conventionally
// means "append" for arrays, per RFC 6902) is forwarded verbatim as the key
// argument. Implementations that model an array-like container are expected
// to give "-" the append semantics; implementations that do not should return
// an error wrapping [ErrDashToken] (or [ErrPointer]) for clarity.
//
// Implementations are responsible for any in-place mutation: the library does
// not attempt to rebind the result of JSONSet into a parent container.
type JSONSetable interface {
// JSONSet sets the value pointed at the (unescaped) key.
//
// The key may be the RFC 6901 "-" token when the pointer targets a
// slice-like member; see the interface documentation for details.
JSONSet(key string, value any) error
}
// NameProvider knows how to resolve go struct fields into json names.
//
// The default provider is brought by [github.com/go-openapi/swag/jsonname.DefaultJSONNameProvider].
type NameProvider interface {
// GetGoName gets the go name for a json property name
GetGoName(subject any, name string) (string, bool)
// GetGoNameForType gets the go name for a given type for a json property name
GetGoNameForType(tpe reflect.Type, name string) (string, bool)
}
+86
View File
@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package jsonpointer
import (
"sync"
"github.com/go-openapi/swag/jsonname"
)
// Option to tune the behavior of a JSON [Pointer].
type Option func(*options)
var (
//nolint:gochecknoglobals // package level defaults are provided as a convenient, backward-compatible way to adopt options.
defaultOptions = options{
provider: jsonname.DefaultJSONNameProvider,
}
//nolint:gochecknoglobals // guards defaultOptions against concurrent SetDefaultNameProvider / read races (testing)
defaultOptionsMu sync.RWMutex
)
// SetDefaultNameProvider sets the [NameProvider] as a package-level default.
//
// By default, the default provider is [jsonname.DefaultJSONNameProvider].
//
// It is safe to call concurrently with [Pointer.Get], [Pointer.Set],
// [GetForToken] and [SetForToken]. The typical usage is to call it once
// at initialization time.
//
// A nil provider is ignored.
func SetDefaultNameProvider(provider NameProvider) {
if provider == nil {
return
}
defaultOptionsMu.Lock()
defer defaultOptionsMu.Unlock()
defaultOptions.provider = provider
}
// UseGoNameProvider sets the [NameProvider] as a package-level default
// to the alternative provider [jsonname.GoNameProvider], that covers a few areas
// not supported by the default name provider.
//
// This implementation supports untagged exported fields and embedded types in go struct.
// It follows strictly the behavior of the JSON standard library regarding field naming conventions.
//
// It is safe to call concurrently with [Pointer.Get], [Pointer.Set],
// [GetForToken] and [SetForToken]. The typical usage is to call it once
// at initialization time.
func UseGoNameProvider() {
SetDefaultNameProvider(jsonname.NewGoNameProvider())
}
// DefaultNameProvider returns the current package-level [NameProvider].
func DefaultNameProvider() NameProvider { //nolint:ireturn // returning the interface is the point — callers pick their own implementation.
defaultOptionsMu.RLock()
defer defaultOptionsMu.RUnlock()
return defaultOptions.provider
}
// WithNameProvider injects a custom [NameProvider] to resolve json names from go struct types.
func WithNameProvider(provider NameProvider) Option {
return func(o *options) {
o.provider = provider
}
}
type options struct {
provider NameProvider
}
func optionsWithDefaults(opts []Option) options {
var o options
o.provider = DefaultNameProvider()
for _, apply := range opts {
apply(&o)
}
return o
}
+273 -85
View File
@@ -11,8 +11,6 @@ import (
"reflect"
"strconv"
"strings"
"github.com/go-openapi/swag/jsonname"
)
const (
@@ -20,20 +18,6 @@ const (
pointerSeparator = `/`
)
// JSONPointable is an interface for structs to implement,
// when they need to customize the json pointer process or want to avoid the use of reflection.
type JSONPointable interface {
// JSONLookup returns a value pointed at this (unescaped) key.
JSONLookup(key string) (any, error)
}
// JSONSetable is an interface for structs to implement,
// when they need to customize the json pointer process or want to avoid the use of reflection.
type JSONSetable interface {
// JSONSet sets the value pointed at the (unescaped) key.
JSONSet(key string, value any) error
}
// Pointer is a representation of a json pointer.
//
// Use [Pointer.Get] to retrieve a value or [Pointer.Set] to set a value.
@@ -41,7 +25,7 @@ type JSONSetable interface {
// It works with any go type interpreted as a JSON document, which means:
//
// - if a type implements [JSONPointable], its [JSONPointable.JSONLookup] method is used to resolve [Pointer.Get]
// - if a type implements [JSONSetable], its [JSONPointable.JSONSet] method is used to resolve [Pointer.Set]
// - if a type implements [JSONSetable], its [JSONSetable.JSONSet] method is used to resolve [Pointer.Set]
// - a go map[K]V is interpreted as an object, with type K assignable to a string
// - a go slice []T is interpreted as an array
// - a go struct is interpreted as an object, with exported fields interpreted as keys
@@ -71,16 +55,35 @@ func New(jsonPointerString string) (Pointer, error) {
// Get uses the pointer to retrieve a value from a JSON document.
//
// It returns the value with its type as a [reflect.Kind] or an error.
func (p *Pointer) Get(document any) (any, reflect.Kind, error) {
return p.get(document, jsonname.DefaultJSONNameProvider)
func (p *Pointer) Get(document any, opts ...Option) (any, reflect.Kind, error) {
o := optionsWithDefaults(opts)
return p.get(document, o.provider)
}
// Set uses the pointer to set a value from a data type
// that represent a JSON document.
//
// It returns the updated document.
func (p *Pointer) Set(document any, value any) (any, error) {
return document, p.set(document, value, jsonname.DefaultJSONNameProvider)
// # Mutation contract
//
// Set mutates the provided document in place whenever Go's type system allows
// it: when document is a map, a pointer, or when the targeted value is reached
// through an addressable ancestor (e.g. a struct field traversed via a pointer,
// a slice element). Callers that rely on this in-place behavior may continue
// to ignore the returned document.
//
// The returned document is only load-bearing when Set cannot mutate in place.
// This happens in one specific case: appending to a top-level slice passed by
// value (e.g. document of type []T rather than *[]T) via the RFC 6901 "-"
// terminal token. reflect.Append produces a new slice header that the library
// cannot rebind into the caller's variable; the updated document is returned
// instead. Pass *[]T if you want in-place rebind for that case as well.
//
// See [ErrDashToken] for the semantics of the "-" token.
func (p *Pointer) Set(document any, value any, opts ...Option) (any, error) {
o := optionsWithDefaults(opts)
return p.set(document, value, o.provider)
}
// DecodedTokens returns the decoded (unescaped) tokens of this JSON pointer.
@@ -109,6 +112,46 @@ func (p *Pointer) String() string {
return pointerSeparator + strings.Join(p.referenceTokens, pointerSeparator)
}
// Offset returns the byte offset, in the raw JSON text of document, of the
// location referenced by this pointer's terminal token.
//
// Unlike [Pointer.Get] and [Pointer.Set], which operate on a decoded Go value,
// Offset operates directly on the textual JSON source. It drives an
// [encoding/json.Decoder] over the string and stops at the terminal token,
// returning the position at which the decoder was about to read that token.
//
// It is primarily intended for tooling that needs to map a pointer back to a
// region of the original source: reporting line/column for validation or
// parse diagnostics, extracting a sub-document by slicing the raw bytes, or
// highlighting the referenced span in an editor.
//
// # Offset semantics
//
// The meaning of the returned offset depends on whether the terminal token
// addresses an object property or an array element:
//
// - Object property: the offset points to the first byte of the key (its
// opening quote character), not to the associated value. For example,
// pointer "/foo/bar" against {"foo": {"bar": 21}} returns 9, the index of
// the opening quote of "bar".
// - Array element: the offset points to the first byte of the value at that
// index. For example, pointer "/0/1" against [[1,2], [3,4]] returns 4,
// the index of the digit 2.
//
// # Errors
//
// Offset returns an error in any of these cases:
//
// - document is not syntactically valid JSON;
// - the structure of document does not match the pointer (e.g. traversing
// into a scalar, or a token that is neither a valid key nor a valid
// numeric index);
// - a referenced key or index does not exist in document;
// - the pointer's terminal token is the RFC 6901 "-" array token, which
// designates a nonexistent element and therefore has no offset in the
// source. The returned error wraps [ErrDashToken].
//
// All errors wrap [ErrPointer].
func (p *Pointer) Offset(document string) (int64, error) {
dec := json.NewDecoder(strings.NewReader(document))
var offset int64
@@ -137,7 +180,35 @@ func (p *Pointer) Offset(document string) (int64, error) {
return 0, fmt.Errorf("invalid token %#v: %w", tk, ErrPointer)
}
}
return offset, nil
return skipJSONSeparator(document, offset), nil
}
// skipJSONSeparator advances offset past trailing JSON whitespace and at most
// one value separator (comma) in document, so the result points at the first
// byte of the next JSON token.
//
// The streaming decoder's InputOffset sits right after the most recently
// consumed token, which between values is the comma (or whitespace) — not
// the following token. Normalizing here keeps Offset's contract uniform:
// for both object keys and array elements, and regardless of position within
// the parent container, the returned offset always points at the first byte
// of the addressed token.
func skipJSONSeparator(document string, offset int64) int64 {
n := int64(len(document))
for offset < n && isJSONWhitespace(document[offset]) {
offset++
}
if offset < n && document[offset] == ',' {
offset++
}
for offset < n && isJSONWhitespace(document[offset]) {
offset++
}
return offset
}
func isJSONWhitespace(c byte) bool {
return c == ' ' || c == '\t' || c == '\n' || c == '\r'
}
// "Constructor", parses the given string JSON pointer.
@@ -157,9 +228,9 @@ func (p *Pointer) parse(jsonPointerString string) error {
return nil
}
func (p *Pointer) get(node any, nameProvider *jsonname.NameProvider) (any, reflect.Kind, error) {
func (p *Pointer) get(node any, nameProvider NameProvider) (any, reflect.Kind, error) {
if nameProvider == nil {
nameProvider = jsonname.DefaultJSONNameProvider
nameProvider = defaultOptions.provider
}
kind := reflect.Invalid
@@ -185,50 +256,130 @@ func (p *Pointer) get(node any, nameProvider *jsonname.NameProvider) (any, refle
return node, kind, nil
}
func (p *Pointer) set(node, data any, nameProvider *jsonname.NameProvider) error {
func (p *Pointer) set(node, data any, nameProvider NameProvider) (any, error) {
knd := reflect.ValueOf(node).Kind()
if knd != reflect.Pointer && knd != reflect.Struct && knd != reflect.Map && knd != reflect.Slice && knd != reflect.Array {
return errors.Join(
return node, errors.Join(
fmt.Errorf("unexpected type: %T", node), //nolint:err113 // err wrapping is carried out by errors.Join, not fmt.Errorf.
ErrUnsupportedValueType,
ErrPointer,
)
}
l := len(p.referenceTokens)
// full document when empty
if l == 0 {
return nil
if len(p.referenceTokens) == 0 {
return node, nil
}
if nameProvider == nil {
nameProvider = jsonname.DefaultJSONNameProvider
nameProvider = defaultOptions.provider
}
var decodedToken string
lastIndex := l - 1
if lastIndex > 0 { // skip if we only have one token in pointer
for _, token := range p.referenceTokens[:lastIndex] {
decodedToken = Unescape(token)
next, err := p.resolveNodeForToken(node, decodedToken, nameProvider)
if err != nil {
return err
}
node = next
}
}
// last token
decodedToken = Unescape(p.referenceTokens[lastIndex])
return setSingleImpl(node, data, decodedToken, nameProvider)
return p.setAt(node, p.referenceTokens, data, nameProvider)
}
func (p *Pointer) resolveNodeForToken(node any, decodedToken string, nameProvider *jsonname.NameProvider) (next any, err error) {
// setAt recursively walks the token list, setting the data at the terminal
// token and rebinding any new child reference (e.g. a slice header returned
// by an "-" append) into its parent on the way back up.
//
// Returning the (possibly new) node at each level is what makes append work
// at any depth without requiring the caller to pass a pointer to the
// containing slice: the new slice header propagates up and each parent
// rebinds it via the appropriate kind-specific setter.
func (p *Pointer) setAt(node any, tokens []string, data any, nameProvider NameProvider) (any, error) {
decodedToken := Unescape(tokens[0])
if len(tokens) == 1 {
return setSingleImpl(node, data, decodedToken, nameProvider)
}
child, err := p.resolveNodeForToken(node, decodedToken, nameProvider)
if err != nil {
return node, err
}
newChild, err := p.setAt(child, tokens[1:], data, nameProvider)
if err != nil {
return node, err
}
return rebindChild(node, decodedToken, newChild, nameProvider)
}
// rebindChild writes newChild back into node at decodedToken.
//
// For cases where the child was already mutated in place (pointer aliasing,
// addressable slice elements) the rebind is a safe no-op. For cases where
// the child was returned by value (map entries holding a slice, slices
// reached through a non-addressable ancestor), the rebind propagates the
// new value into the parent.
//
// Parents implementing [JSONPointable] are left alone: they took ownership
// of the child via JSONLookup and did not opt into a JSONSet-based rebind
// on intermediate tokens.
func rebindChild(node any, decodedToken string, newChild any, nameProvider NameProvider) (any, error) {
if _, ok := node.(JSONPointable); ok {
return node, nil
}
rValue := reflect.Indirect(reflect.ValueOf(node))
switch rValue.Kind() {
case reflect.Struct:
nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken)
if !ok {
return node, fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer)
}
fld := rValue.FieldByName(nm)
if !fld.CanSet() {
return node, nil
}
assignReflectValue(fld, newChild)
return node, nil
case reflect.Map:
rValue.SetMapIndex(reflect.ValueOf(decodedToken), reflect.ValueOf(newChild))
return node, nil
case reflect.Slice:
if decodedToken == dashToken {
return node, errDashIntermediate()
}
idx, err := strconv.Atoi(decodedToken)
if err != nil {
return node, errors.Join(err, ErrPointer)
}
elem := rValue.Index(idx)
if !elem.CanSet() {
return node, nil
}
assignReflectValue(elem, newChild)
return node, nil
default:
return node, errInvalidReference(decodedToken)
}
}
// assignReflectValue assigns src into dst, unwrapping a pointer when dst
// expects the pointee type. This tolerates the pointer-wrapping performed
// by [typeFromValue] for addressable fields.
func assignReflectValue(dst reflect.Value, src any) {
nv := reflect.ValueOf(src)
if !nv.IsValid() {
return
}
if nv.Type().AssignableTo(dst.Type()) {
dst.Set(nv)
return
}
if nv.Kind() == reflect.Pointer && nv.Elem().Type().AssignableTo(dst.Type()) {
dst.Set(nv.Elem())
}
}
func (p *Pointer) resolveNodeForToken(node any, decodedToken string, nameProvider NameProvider) (next any, err error) {
// check for nil during traversal
if isNil(node) {
return nil, fmt.Errorf("cannot traverse through nil value at %q: %w", decodedToken, ErrPointer)
@@ -272,6 +423,9 @@ func (p *Pointer) resolveNodeForToken(node any, decodedToken string, nameProvide
return typeFromValue(mv), nil
case reflect.Slice:
if decodedToken == dashToken {
return nil, errDashIntermediate()
}
tokenIndex, err := strconv.Atoi(decodedToken)
if err != nil {
return nil, errors.Join(err, ErrPointer)
@@ -312,16 +466,23 @@ func typeFromValue(v reflect.Value) any {
}
// GetForToken gets a value for a json pointer token 1 level deep.
func GetForToken(document any, decodedToken string) (any, reflect.Kind, error) {
return getSingleImpl(document, decodedToken, jsonname.DefaultJSONNameProvider)
func GetForToken(document any, decodedToken string, opts ...Option) (any, reflect.Kind, error) {
o := optionsWithDefaults(opts)
return getSingleImpl(document, decodedToken, o.provider)
}
// SetForToken sets a value for a json pointer token 1 level deep.
func SetForToken(document any, decodedToken string, value any) (any, error) {
return document, setSingleImpl(document, value, decodedToken, jsonname.DefaultJSONNameProvider)
//
// See [Pointer.Set] for the mutation contract, in particular the handling of
// the RFC 6901 "-" token on slices.
func SetForToken(document any, decodedToken string, value any, opts ...Option) (any, error) {
o := optionsWithDefaults(opts)
return setSingleImpl(document, value, decodedToken, o.provider)
}
func getSingleImpl(node any, decodedToken string, nameProvider *jsonname.NameProvider) (any, reflect.Kind, error) {
func getSingleImpl(node any, decodedToken string, nameProvider NameProvider) (any, reflect.Kind, error) {
rValue := reflect.Indirect(reflect.ValueOf(node))
kind := rValue.Kind()
if isNil(node) {
@@ -361,6 +522,9 @@ func getSingleImpl(node any, decodedToken string, nameProvider *jsonname.NamePro
return nil, kind, errNoKey(decodedToken)
case reflect.Slice:
if decodedToken == dashToken {
return nil, kind, errDashOnGet()
}
tokenIndex, err := strconv.Atoi(decodedToken)
if err != nil {
return nil, kind, errors.Join(err, ErrPointer)
@@ -378,14 +542,14 @@ func getSingleImpl(node any, decodedToken string, nameProvider *jsonname.NamePro
}
}
func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.NameProvider) error {
func setSingleImpl(node, data any, decodedToken string, nameProvider NameProvider) (any, error) {
// check for nil to prevent panic when calling rValue.Type()
if isNil(node) {
return fmt.Errorf("cannot set field %q on nil value: %w", decodedToken, ErrPointer)
return node, fmt.Errorf("cannot set field %q on nil value: %w", decodedToken, ErrPointer)
}
if ns, ok := node.(JSONSetable); ok {
return ns.JSONSet(decodedToken, data)
return node, ns.JSONSet(decodedToken, data)
}
rValue := reflect.Indirect(reflect.ValueOf(node))
@@ -394,12 +558,12 @@ func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.N
case reflect.Struct:
nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken)
if !ok {
return fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer)
return node, fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer)
}
fld := rValue.FieldByName(nm)
if !fld.CanSet() {
return fmt.Errorf("can't set struct field %s to %v: %w", nm, data, ErrPointer)
return node, fmt.Errorf("can't set struct field %s to %v: %w", nm, data, ErrPointer)
}
value := reflect.ValueOf(data)
@@ -407,33 +571,51 @@ func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.N
assignedType := fld.Type()
if !valueType.AssignableTo(assignedType) {
return fmt.Errorf("can't set value with type %T to field %s with type %v: %w", data, nm, assignedType, ErrPointer)
return node, fmt.Errorf("can't set value with type %T to field %s with type %v: %w", data, nm, assignedType, ErrPointer)
}
fld.Set(value)
return nil
return node, nil
case reflect.Map:
kv := reflect.ValueOf(decodedToken)
rValue.SetMapIndex(kv, reflect.ValueOf(data))
return nil
return node, nil
case reflect.Slice:
if decodedToken == dashToken {
// RFC 6901 §4 / RFC 6902 append semantics: terminal "-" appends
// the value to the slice. We rebind in place when the slice is
// reachable via an addressable ancestor; otherwise we return the
// new slice header for the parent (or the public Set) to rebind.
value := reflect.ValueOf(data)
elemType := rValue.Type().Elem()
if !value.Type().AssignableTo(elemType) {
return node, fmt.Errorf("can't append value of type %T to slice of %v: %w", data, elemType, ErrPointer)
}
newSlice := reflect.Append(rValue, value)
if rValue.CanSet() {
rValue.Set(newSlice)
return node, nil
}
return newSlice.Interface(), nil
}
tokenIndex, err := strconv.Atoi(decodedToken)
if err != nil {
return errors.Join(err, ErrPointer)
return node, errors.Join(err, ErrPointer)
}
sLength := rValue.Len()
if tokenIndex < 0 || tokenIndex >= sLength {
return errOutOfBounds(sLength, tokenIndex)
return node, errOutOfBounds(sLength, tokenIndex)
}
elem := rValue.Index(tokenIndex)
if !elem.CanSet() {
return fmt.Errorf("can't set slice index %s to %v: %w", decodedToken, data, ErrPointer)
return node, fmt.Errorf("can't set slice index %s to %v: %w", decodedToken, data, ErrPointer)
}
value := reflect.ValueOf(data)
@@ -441,15 +623,15 @@ func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.N
assignedType := elem.Type()
if !valueType.AssignableTo(assignedType) {
return fmt.Errorf("can't set value with type %T to slice element %d with type %v: %w", data, tokenIndex, assignedType, ErrPointer)
return node, fmt.Errorf("can't set value with type %T to slice element %d with type %v: %w", data, tokenIndex, assignedType, ErrPointer)
}
elem.Set(value)
return nil
return node, nil
default:
return errInvalidReference(decodedToken)
return node, errInvalidReference(decodedToken)
}
}
@@ -460,24 +642,27 @@ func offsetSingleObject(dec *json.Decoder, decodedToken string) (int64, error) {
if err != nil {
return 0, err
}
switch tk := tk.(type) {
case json.Delim:
switch tk {
case '{':
if err = drainSingle(dec); err != nil {
return 0, err
}
case '[':
key, ok := tk.(string)
if !ok {
return 0, fmt.Errorf("invalid key token %#v: %w", tk, ErrPointer)
}
if key == decodedToken {
return offset, nil
}
// Consume the associated value. Scalars are fully read by a single
// Token() call; composite values must be drained.
tk, err = dec.Token()
if err != nil {
return 0, err
}
if delim, isDelim := tk.(json.Delim); isDelim {
switch delim {
case '{', '[':
if err = drainSingle(dec); err != nil {
return 0, err
}
}
case string:
if tk == decodedToken {
return offset, nil
}
default:
return 0, fmt.Errorf("invalid token %#v: %w", tk, ErrPointer)
}
}
@@ -485,6 +670,9 @@ func offsetSingleObject(dec *json.Decoder, decodedToken string) (int64, error) {
}
func offsetSingleArray(dec *json.Decoder, decodedToken string) (int64, error) {
if decodedToken == dashToken {
return 0, errDashOnOffset()
}
idx, err := strconv.Atoi(decodedToken)
if err != nil {
return 0, fmt.Errorf("token reference %q is not a number: %w: %w", decodedToken, err, ErrPointer)
-1
View File
@@ -3,4 +3,3 @@
.idea
.env
.mcp.json
.claude/
+11 -11
View File
@@ -4,18 +4,18 @@
| Total Contributors | Total Contributions |
| --- | --- |
| 9 | 73 |
| 9 | 79 |
| Username | All Time Contribution Count | All Commits |
| --- | --- | --- |
| @fredbi | 36 | https://github.com/go-openapi/jsonreference/commits?author=fredbi |
| @casualjim | 25 | https://github.com/go-openapi/jsonreference/commits?author=casualjim |
| @youyuanwu | 5 | https://github.com/go-openapi/jsonreference/commits?author=youyuanwu |
| @olivierlemasle | 2 | https://github.com/go-openapi/jsonreference/commits?author=olivierlemasle |
| @apelisse | 1 | https://github.com/go-openapi/jsonreference/commits?author=apelisse |
| @gbjk | 1 | https://github.com/go-openapi/jsonreference/commits?author=gbjk |
| @honza | 1 | https://github.com/go-openapi/jsonreference/commits?author=honza |
| @Neo2308 | 1 | https://github.com/go-openapi/jsonreference/commits?author=Neo2308 |
| @erraggy | 1 | https://github.com/go-openapi/jsonreference/commits?author=erraggy |
| @fredbi | 42 | <https://github.com/go-openapi/jsonreference/commits?author=fredbi> |
| @casualjim | 25 | <https://github.com/go-openapi/jsonreference/commits?author=casualjim> |
| @youyuanwu | 5 | <https://github.com/go-openapi/jsonreference/commits?author=youyuanwu> |
| @olivierlemasle | 2 | <https://github.com/go-openapi/jsonreference/commits?author=olivierlemasle> |
| @apelisse | 1 | <https://github.com/go-openapi/jsonreference/commits?author=apelisse> |
| @gbjk | 1 | <https://github.com/go-openapi/jsonreference/commits?author=gbjk> |
| @honza | 1 | <https://github.com/go-openapi/jsonreference/commits?author=honza> |
| @Neo2308 | 1 | <https://github.com/go-openapi/jsonreference/commits?author=Neo2308> |
| @erraggy | 1 | <https://github.com/go-openapi/jsonreference/commits?author=erraggy> |
_this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_
_this file was generated by the [Contributors GitHub Action](https://github.com/github-community-projects/contributors)_
+10 -12
View File
@@ -14,15 +14,9 @@
An implementation of JSON Reference for golang.
<!--
## Announcements
* **2025-12-19** : new community chat on discord
* a new discord community channel is available to be notified of changes and support users
* our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31**
You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url]
Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url]
-->
## Status
@@ -74,9 +68,9 @@ on top of which it has been built.
## Other documentation
* [All-time contributors](./CONTRIBUTORS.md)
* [Contributing guidelines](.github/CONTRIBUTING.md)
* [Maintainers documentation](docs/MAINTAINERS.md)
* [Code style](docs/STYLE.md)
* [Contributing guidelines][contributing-doc-site]
* [Maintainers documentation][maintainers-doc-site]
* [Code style][style-doc-site]
## Cutting a new release
@@ -115,7 +109,7 @@ Maintainers can cut a new release by either:
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
[discord-url]: https://discord.gg/twZ9BwT3
[discord-url]: https://discord.gg/FfnFYaC3k5
<!-- Badges: license & compliance -->
[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
@@ -125,3 +119,7 @@ Maintainers can cut a new release by either:
[goversion-url]: https://github.com/go-openapi/jsonreference/blob/master/go.mod
[top-badge]: https://img.shields.io/github/languages/top/go-openapi/jsonreference
[commits-badge]: https://img.shields.io/github/commits-since/go-openapi/jsonreference/latest
<!-- Organization docs -->
[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
+9
View File
@@ -0,0 +1,9 @@
codecov:
notify:
after_n_builds: 2
coverage:
status:
patch:
default:
target: 80%
+2
View File
@@ -3,3 +3,5 @@
.idea
.env
.mcp.json
go.work.sum
.worktrees/
+20 -10
View File
@@ -2,13 +2,9 @@ version: "2"
linters:
default: all
disable:
- cyclop
- depguard
- err113 # disabled temporarily: there are just too many issues to address
- errchkjson
- errorlint
- exhaustruct
- forcetypeassert
- funlen
- gochecknoglobals
- gochecknoinits
@@ -16,12 +12,12 @@ linters:
- godot
- godox
- gomoddirectives # moved to mono-repo, multi-modules, so replace directives are needed
- gomodguard
- gomodguard_v2
- gosmopolitan
- inamedparam
- ireturn
- lll
- ireturn # this repo adopted a pattern where there are quite many returned interfaces. To be challenged with v2
- musttag
- nestif
- nilerr # nilerr crashes on this repo
- nlreturn
- noinlineerr
@@ -31,7 +27,6 @@ linters:
- testpackage
- thelper
- tparallel
- unparam
- varnamelen
- whitespace
- wrapcheck
@@ -43,8 +38,17 @@ linters:
goconst:
min-len: 2
min-occurrences: 3
cyclop:
max-complexity: 25
gocyclo:
min-complexity: 45
min-complexity: 25
gocognit:
min-complexity: 35
exhaustive:
default-signifies-exhaustive: true
default-case-required: true
lll:
line-length: 180
exclusions:
generated: lax
presets:
@@ -53,6 +57,7 @@ linters:
- legacy
- std-error-handling
paths:
- .worktrees
- third_party$
- builtin$
- examples$
@@ -60,12 +65,17 @@ 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$
- examples$
issues:
# Maximum issues count per one linter.
# Set to 0 to disable.
+83
View File
@@ -0,0 +1,83 @@
# Contributors
- Repository: ['go-openapi/runtime']
| Total Contributors | Total Contributions |
| --- | --- |
| 71 | 565 |
| Username | All Time Contribution Count | All Commits |
| --- | --- | --- |
| @casualjim | 268 | <https://github.com/go-openapi/runtime/commits?author=casualjim> |
| @fredbi | 140 | <https://github.com/go-openapi/runtime/commits?author=fredbi> |
| @youyuanwu | 19 | <https://github.com/go-openapi/runtime/commits?author=youyuanwu> |
| @josephwoodward | 13 | <https://github.com/go-openapi/runtime/commits?author=josephwoodward> |
| @kenjones-cisco | 12 | <https://github.com/go-openapi/runtime/commits?author=kenjones-cisco> |
| @GlenDC | 7 | <https://github.com/go-openapi/runtime/commits?author=GlenDC> |
| @moenning | 6 | <https://github.com/go-openapi/runtime/commits?author=moenning> |
| @mstoykov | 6 | <https://github.com/go-openapi/runtime/commits?author=mstoykov> |
| @elakito | 6 | <https://github.com/go-openapi/runtime/commits?author=elakito> |
| @ifraixedes | 5 | <https://github.com/go-openapi/runtime/commits?author=ifraixedes> |
| @zeitlinger | 4 | <https://github.com/go-openapi/runtime/commits?author=zeitlinger> |
| @Copilot | 3 | <https://github.com/go-openapi/runtime/commits?author=Copilot> |
| @jkawamoto | 3 | <https://github.com/go-openapi/runtime/commits?author=jkawamoto> |
| @stoyanr | 3 | <https://github.com/go-openapi/runtime/commits?author=stoyanr> |
| @keramix | 2 | <https://github.com/go-openapi/runtime/commits?author=keramix> |
| @Equanox | 2 | <https://github.com/go-openapi/runtime/commits?author=Equanox> |
| @ederavilaprado | 2 | <https://github.com/go-openapi/runtime/commits?author=ederavilaprado> |
| @nan0tube | 2 | <https://github.com/go-openapi/runtime/commits?author=nan0tube> |
| @thomdixon | 2 | <https://github.com/go-openapi/runtime/commits?author=thomdixon> |
| @deborggraever | 2 | <https://github.com/go-openapi/runtime/commits?author=deborggraever> |
| @MakarandNsd | 2 | <https://github.com/go-openapi/runtime/commits?author=MakarandNsd> |
| @Vadskye | 2 | <https://github.com/go-openapi/runtime/commits?author=Vadskye> |
| @jsilland | 2 | <https://github.com/go-openapi/runtime/commits?author=jsilland> |
| @Kunde21 | 2 | <https://github.com/go-openapi/runtime/commits?author=Kunde21> |
| @bcomnes | 2 | <https://github.com/go-openapi/runtime/commits?author=bcomnes> |
| @galaxie | 2 | <https://github.com/go-openapi/runtime/commits?author=galaxie> |
| @anfernee | 2 | <https://github.com/go-openapi/runtime/commits?author=anfernee> |
| @wahabmk | 1 | <https://github.com/go-openapi/runtime/commits?author=wahabmk> |
| @vearutop | 1 | <https://github.com/go-openapi/runtime/commits?author=vearutop> |
| @tschaub | 1 | <https://github.com/go-openapi/runtime/commits?author=tschaub> |
| @pytlesk4 | 1 | <https://github.com/go-openapi/runtime/commits?author=pytlesk4> |
| @tgraf | 1 | <https://github.com/go-openapi/runtime/commits?author=tgraf> |
| @seanprince | 1 | <https://github.com/go-openapi/runtime/commits?author=seanprince> |
| @rodriguise | 1 | <https://github.com/go-openapi/runtime/commits?author=rodriguise> |
| @petrkotas | 1 | <https://github.com/go-openapi/runtime/commits?author=petrkotas> |
| @maxatome | 1 | <https://github.com/go-openapi/runtime/commits?author=maxatome> |
| @maxkarelov | 1 | <https://github.com/go-openapi/runtime/commits?author=maxkarelov> |
| @tooolbox | 1 | <https://github.com/go-openapi/runtime/commits?author=tooolbox> |
| @akutz | 1 | <https://github.com/go-openapi/runtime/commits?author=akutz> |
| @yabberyabber | 1 | <https://github.com/go-openapi/runtime/commits?author=yabberyabber> |
| @elv-gilles | 1 | <https://github.com/go-openapi/runtime/commits?author=elv-gilles> |
| @gregmarr | 1 | <https://github.com/go-openapi/runtime/commits?author=gregmarr> |
| @jwalter1-quest | 1 | <https://github.com/go-openapi/runtime/commits?author=jwalter1-quest> |
| @s4s7 | 1 | <https://github.com/go-openapi/runtime/commits?author=s4s7> |
| @stingshen | 1 | <https://github.com/go-openapi/runtime/commits?author=stingshen> |
| @tamalsaha | 1 | <https://github.com/go-openapi/runtime/commits?author=tamalsaha> |
| @tte | 1 | <https://github.com/go-openapi/runtime/commits?author=tte> |
| @martian4202 | 1 | <https://github.com/go-openapi/runtime/commits?author=martian4202> |
| @yan-zhuang | 1 | <https://github.com/go-openapi/runtime/commits?author=yan-zhuang> |
| @aleksandr-vin | 1 | <https://github.com/go-openapi/runtime/commits?author=aleksandr-vin> |
| @azylman | 1 | <https://github.com/go-openapi/runtime/commits?author=azylman> |
| @anasmuhmd | 1 | <https://github.com/go-openapi/runtime/commits?author=anasmuhmd> |
| @ArFe | 1 | <https://github.com/go-openapi/runtime/commits?author=ArFe> |
| @CodeLingoBot | 1 | <https://github.com/go-openapi/runtime/commits?author=CodeLingoBot> |
| @dlmiddlecote | 1 | <https://github.com/go-openapi/runtime/commits?author=dlmiddlecote> |
| @danny-cheung | 1 | <https://github.com/go-openapi/runtime/commits?author=danny-cheung> |
| @calavera | 1 | <https://github.com/go-openapi/runtime/commits?author=calavera> |
| @EdwardBetts | 1 | <https://github.com/go-openapi/runtime/commits?author=EdwardBetts> |
| @etsangsplk | 1 | <https://github.com/go-openapi/runtime/commits?author=etsangsplk> |
| @ericzsplk | 1 | <https://github.com/go-openapi/runtime/commits?author=ericzsplk> |
| @faguirre1 | 1 | <https://github.com/go-openapi/runtime/commits?author=faguirre1> |
| @florindragos | 1 | <https://github.com/go-openapi/runtime/commits?author=florindragos> |
| @gbjk | 1 | <https://github.com/go-openapi/runtime/commits?author=gbjk> |
| @taisho6339 | 1 | <https://github.com/go-openapi/runtime/commits?author=taisho6339> |
| @jbowes | 1 | <https://github.com/go-openapi/runtime/commits?author=jbowes> |
| @JoakimSoderberg | 1 | <https://github.com/go-openapi/runtime/commits?author=JoakimSoderberg> |
| @robbert229 | 1 | <https://github.com/go-openapi/runtime/commits?author=robbert229> |
| @jonathaningram | 1 | <https://github.com/go-openapi/runtime/commits?author=jonathaningram> |
| @KuaaMU | 1 | <https://github.com/go-openapi/runtime/commits?author=KuaaMU> |
| @germanhs | 1 | <https://github.com/go-openapi/runtime/commits?author=germanhs> |
| @pracucci | 1 | <https://github.com/go-openapi/runtime/commits?author=pracucci> |
_this file was generated by the [Contributors GitHub Action](https://github.com/github-community-projects/contributors)_
+56 -20
View File
@@ -8,8 +8,7 @@
[![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url]
<!-- Badges: documentation & support -->
<!-- Badges: others & stats -->
[![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]
[![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.
@@ -18,13 +17,44 @@ The runtime component for use in code generation or as untyped usage.
## Announcements
* **2025-12-19** : new community chat on discord
* a new discord community channel is available to be notified of changes and support users
* our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31**
[**Complete documentation as github pages**][doc-url]
You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url]
**Changes to the API surface in `v0.30.0`**:
Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url]
* 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
@@ -40,18 +70,21 @@ go get github.com/go-openapi/runtime
See <https://github.com/go-openapi/runtime/releases>
For pre-v0.30.0 releases see [release notes](docs/NOTES.md).
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 :
* [ ] continue narrowing down the scope of dependencies:
* yaml support in an independent module
* [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).
* [ ] fix a few known issues with some file upload requests (e.g. #286)
will remain maintained, but evolve at a slower pace than opentelemetry). (v2)
* [] publish proper documentation and examples
## Licensing
@@ -62,11 +95,11 @@ on top of which it has been built.
## Other documentation
* [FAQ](docs/FAQ.md)
* [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](.github/CONTRIBUTING.md)
* [Maintainers documentation](docs/MAINTAINERS.md)
* [Code style](docs/STYLE.md)
* [Contributing guidelines][contributing-doc-site]
* [Maintainers documentation][maintainers-doc-site]
* [Code style][style-doc-site]
## Cutting a new release
@@ -95,13 +128,12 @@ Maintainers can cut a new release by either:
[codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/runtime
[codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/runtime
<!-- Badges: documentation & support -->
[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
[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
[discord-url]: https://discord.gg/twZ9BwT3
[discord-url]: https://discord.gg/FfnFYaC3k5
<!-- Badges: license & compliance -->
[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
@@ -111,3 +143,7 @@ Maintainers can cut a new release by either:
[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
<!-- Organization docs -->
[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
+8 -8
View File
@@ -97,7 +97,7 @@ func ByteStreamConsumer(opts ...byteStreamOpt) Consumer {
}
default:
// check for the underlying type to be pointer to []byte or string,
if ptr := reflect.TypeOf(data); ptr.Kind() != reflect.Ptr {
if ptr := reflect.TypeOf(data); ptr.Kind() != reflect.Pointer {
return errors.New("destination must be a pointer")
}
@@ -126,13 +126,13 @@ func ByteStreamConsumer(opts ...byteStreamOpt) Consumer {
//
// 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.
// - [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 {
+520
View File
@@ -0,0 +1,520 @@
// 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")
}
+353
View File
@@ -0,0 +1,353 @@
// 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)
}
}
+945
View File
@@ -0,0 +1,945 @@
// 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, ok := producers[mediaType]
if !ok {
return nil, fmt.Errorf("no producer registered for content type %q (register one with Runtime.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
}
+3 -3
View File
@@ -34,20 +34,20 @@ func (k *keepAliveTransport) RoundTrip(r *http.Request) (*http.Response, error)
type drainingReadCloser struct {
rdr io.ReadCloser
seenEOF uint32
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 {
atomic.StoreUint32(&d.seenEOF, 1)
d.seenEOF.Store(1)
}
return
}
func (d *drainingReadCloser) Close() error {
// drain buffer
if atomic.LoadUint32(&d.seenEOF) != 1 {
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.
+85 -8
View File
@@ -4,18 +4,20 @@
package client
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"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 (
@@ -23,6 +25,52 @@ const (
tracerName = "go-openapi"
)
// WithOpenTelemetry adds opentelemetry support to the provided runtime.
// A new client span is created for each request.
// The provided opts are applied to each spans - for example to add global tags.
//
// The returned transport satisfies [runtime.ContextualTransport]: callers
// should prefer [openTelemetryTransport.SubmitContext] over the
// legacy [runtime.ClientOperation.Context] field. Setting that
// field is still honored on the [openTelemetryTransport.Submit]
// compatibility path.
func (r *Runtime) WithOpenTelemetry(opts ...OpenTelemetryOpt) runtime.ContextualTransport {
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.ContextualTransport {
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
@@ -113,11 +161,31 @@ func newOpenTelemetryTransport(transport runtime.ClientTransport, host string, o
return tr
}
// Submit implements [runtime.ClientTransport]. It honors the legacy
// [runtime.ClientOperation.Context] field for backward compatibility
// — that field is being phased out; new code should call
// [openTelemetryTransport.SubmitContext] directly with an explicit
// context.
func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (any, error) {
if op.Context == nil {
return t.transport.Submit(op)
ctx := op.Context
if ctx == nil {
ctx = context.Background()
}
return t.SubmitContext(ctx, op)
}
// SubmitContext submits an operation with an explicit context that
// drives both the tracing span and (when supported) the wrapped
// transport's SubmitContext call. The legacy
// [runtime.ClientOperation.Context] field is not consulted.
//
// When the wrapped transport implements [runtime.ContextualTransport], ctx is
// forwarded directly via its SubmitContext. Otherwise, the legacy
// Submit path is used: ctx is stamped onto op.Context for the
// duration of that call and restored afterwards, so the wrapped
// transport still receives a usable context. The legacy fallback
// disappears once SubmitContext is universal (v2).
func (t *openTelemetryTransport) SubmitContext(ctx context.Context, op *runtime.ClientOperation) (any, error) {
params := op.Params
reader := op.Reader
@@ -129,7 +197,7 @@ func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (any, error
}()
op.Params = runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error {
span = t.newOpenTelemetrySpan(op, req.GetHeaderParams())
span = t.newOpenTelemetrySpan(ctx, op, req.GetHeaderParams())
return params.WriteToRequest(req, reg)
})
@@ -149,7 +217,7 @@ func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (any, error
return reader.ReadResponse(response, consumer)
})
submit, err := t.transport.Submit(op)
submit, err := t.submitWrapped(ctx, op)
if err != nil && span != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
@@ -158,9 +226,18 @@ func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (any, error
return submit, err
}
func (t *openTelemetryTransport) newOpenTelemetrySpan(op *runtime.ClientOperation, header http.Header) trace.Span {
ctx := op.Context
//nolint:contextcheck // ctx is forwarded verbatim; the legacy Submit branch only stamps it onto op.Context for the wrapped transport.
func (t *openTelemetryTransport) submitWrapped(ctx context.Context, op *runtime.ClientOperation) (any, error) {
if sc, ok := t.transport.(runtime.ContextualTransport); ok {
return sc.SubmitContext(ctx, op)
}
prev := op.Context
op.Context = ctx
defer func() { op.Context = prev }()
return t.transport.Submit(op)
}
func (t *openTelemetryTransport) newOpenTelemetrySpan(ctx context.Context, op *runtime.ClientOperation, header http.Header) trace.Span {
tracer := t.tracer
if tracer == nil {
if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {
-468
View File
@@ -1,468 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package client
import (
"bytes"
"context"
"fmt"
"io"
"log"
"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.
//
// This Request struct converts to a HTTP request.
// There might be others that convert to other transports.
// There is no error checking here, it is assumed to be used after a spec has been validated.
// so impossible combinations should not arise (hopefully).
//
// The main purpose of this struct is to hide the machinery of adding params to a transport request.
// The generated code only implements what is necessary to turn a param into a valid value for these methods.
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
timeout time.Duration
buf *bytes.Buffer
getBody func(r *request) []byte
}
// NewRequest creates a new swagger http client request.
func newRequest(method, pathPattern string, writer runtime.ClientRequestWriter) *request {
return &request{
pathPattern: pathPattern,
method: method,
writer: writer,
header: make(http.Header),
query: make(url.Values),
timeout: DefaultTimeout,
getBody: getRequestBuffer,
}
}
// BuildHTTP creates a new http request based on the data from the params.
func (r *request) BuildHTTP(mediaType, basePath string, producers map[string]runtime.Producer, registry strfmt.Registry) (*http.Request, error) {
return r.buildHTTP(mediaType, basePath, producers, registry, nil)
}
func (r *request) GetMethod() string {
return r.method
}
func (r *request) GetPath() string {
path := r.pathPattern
for k, v := range r.pathParams {
path = strings.ReplaceAll(path, "{"+k+"}", v)
}
return path
}
func (r *request) GetBody() []byte {
return r.getBody(r)
}
// SetHeaderParam adds a header param to the request
// when there is only 1 value provided for the varargs, it will set it.
// when there are several values provided for the varargs it will add it (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 the all headers currently set for the request.
func (r *request) GetHeaderParams() http.Header {
return r.header
}
// SetQueryParam adds a query param to the request
// when there is only 1 value provided for the varargs, it will set it.
// when there are several values provided for the varargs it will add it (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 {
var result = make(url.Values)
for key, value := range r.query {
result[key] = append([]string{}, value...)
}
return result
}
// SetFormParam adds a forn param to the request
// when there is only 1 value provided for the varargs, it will set it.
// when there are several values provided for the varargs it will add it (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 param to the request.
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
}
func (r *request) GetFileParam() map[string][]runtime.NamedReadCloser {
return r.fileFields
}
// SetBodyParam sets a body parameter on the request.
// This does not yet serialze the object, this happens as late as possible.
func (r *request) SetBodyParam(payload any) error {
r.payload = payload
return nil
}
func (r *request) GetBodyParam() any {
return r.payload
}
// SetTimeout sets the timeout for a request.
func (r *request) SetTimeout(timeout time.Duration) error {
r.timeout = timeout
return nil
}
func (r *request) isMultipart(mediaType string) bool {
if len(r.fileFields) > 0 {
return true
}
return runtime.MultipartFormMime == mediaType
}
func (r *request) buildHTTP(mediaType, basePath string, producers map[string]runtime.Producer, registry strfmt.Registry, auth runtime.ClientAuthInfoWriter) (*http.Request, error) { //nolint:gocyclo,maintidx
// build the data
if err := r.writer.WriteToRequest(r, registry); err != nil {
return nil, err
}
// Our body must be an io.Reader.
// When we create the http.Request, if we pass it a
// bytes.Buffer then it will wrap it in an io.ReadCloser
// and set the content length automatically.
var body io.Reader
var pr *io.PipeReader
var pw *io.PipeWriter
r.buf = bytes.NewBuffer(nil)
if r.payload != nil || len(r.formFields) > 0 || len(r.fileFields) > 0 {
body = r.buf
if r.isMultipart(mediaType) {
pr, pw = io.Pipe()
body = pr
}
}
// check if this is a form type request
if len(r.formFields) > 0 || len(r.fileFields) > 0 {
if !r.isMultipart(mediaType) {
r.header.Set(runtime.HeaderContentType, mediaType)
formString := r.formFields.Encode()
r.buf.WriteString(formString)
goto DoneChoosingBodySource
}
mp := multipart.NewWriter(pw)
r.header.Set(runtime.HeaderContentType, mangleContentType(mediaType, mp.Boundary()))
go func() {
defer func() {
mp.Close()
pw.Close()
}()
for fn, v := range r.formFields {
for _, vi := range v {
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 {
var fileContentType string
if p, ok := fi.(interface {
ContentType() string
}); 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 && 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, fi); err != nil {
logClose(err, pw)
}
}
}
}()
goto DoneChoosingBodySource
}
// if there is payload, use the producer to write the payload, and then
// set the header to the content-type appropriate for the payload produced
if r.payload != nil {
// Enhancement proposal: https://github.com/go-openapi/runtime/issues/387
r.header.Set(runtime.HeaderContentType, mediaType)
if rdr, ok := r.payload.(io.ReadCloser); ok {
body = rdr
goto DoneChoosingBodySource
}
if rdr, ok := r.payload.(io.Reader); ok {
body = rdr
goto DoneChoosingBodySource
}
producer := producers[mediaType]
if err := producer.Produce(r.buf, r.payload); err != nil {
return nil, err
}
}
DoneChoosingBodySource:
if runtime.CanHaveBody(r.method) && body != nil && r.header.Get(runtime.HeaderContentType) == "" {
r.header.Set(runtime.HeaderContentType, mediaType)
}
if auth != nil {
// If we're not using r.buf as our http.Request's body,
// either the payload is an io.Reader or io.ReadCloser,
// or we're doing a multipart form/file.
//
// In those cases, if the AuthenticateRequest call asks for the body,
// we must read it into a buffer and provide that, then use that buffer
// as the body of our http.Request.
//
// This is done in-line with the GetBody() request rather than ahead
// of time, because there's no way to know if the AuthenticateRequest
// will even ask for the body of the request.
//
// If for some reason the copy fails, there's no way to return that
// error to the GetBody() call, so return it afterwards.
//
// An error from the copy action is prioritized over any error
// from the AuthenticateRequest call, because the mis-read
// body may have interfered with the auth.
//
var copyErr error
if buf, ok := body.(*bytes.Buffer); body != nil && (!ok || buf != r.buf) {
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)
if copyErr != nil {
return nil, fmt.Errorf("error retrieving the response body: %v", copyErr)
}
if authErr != nil {
return nil, authErr
}
}
// In case the basePath or the request pathPattern include static query parameters,
// parse those out before constructing the final path. The parameters themselves
// will be merged with the ones set by the client, with the priority given first to
// the ones set by the client, then the path pattern, and lastly the base path.
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)
}
}
// create http request
var reinstateSlash bool
if pathPatternURL.Path != "" && pathPatternURL.Path != "/" && pathPatternURL.Path[len(pathPatternURL.Path)-1] == '/' {
reinstateSlash = true
}
urlPath := path.Join(basePathURL.Path, pathPatternURL.Path)
for k, v := range r.pathParams {
urlPath = strings.ReplaceAll(urlPath, "{"+k+"}", url.PathEscape(v))
}
if reinstateSlash {
urlPath += "/"
}
req, err := http.NewRequestWithContext(context.Background(), r.method, urlPath, body)
if err != nil {
return nil, err
}
originalParams := r.GetQueryParams()
// Merge the query parameters extracted from the basePath with the ones set by
// the client in this struct. In case of conflict, the client wins.
for k, v := range staticQueryParams {
_, present := originalParams[k]
if !present {
if err = r.SetQueryParam(k, v...); err != nil {
return nil, err
}
}
}
req.URL.RawQuery = r.query.Encode()
req.Header = r.header
return req, nil
}
func escapeQuotes(s string) string {
return strings.NewReplacer("\\", "\\\\", `"`, "\\\"").Replace(s)
}
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 {
if strings.ToLower(mediaType) == runtime.URLencodedFormMime {
return fmt.Sprintf("%s; boundary=%s", mediaType, boundary)
}
return "multipart/form-data; boundary=" + boundary
}
+305 -339
View File
@@ -5,25 +5,19 @@ package client
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"mime"
"net/http"
"net/http/httputil"
"os"
"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"
)
@@ -36,184 +30,6 @@ const (
// DefaultTimeout the default request timeout.
var DefaultTimeout = 30 * time.Second
// 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 with 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: %v", err)
}
cfg.Certificates = []tls.Certificate{cert}
} else if opts.LoadedCertificate != nil {
block := pem.Block{Type: "CERTIFICATE", Bytes: opts.LoadedCertificate.Raw}
certPem := pem.EncodeToMemory(&block)
var keyBytes []byte
switch k := opts.LoadedKey.(type) {
case *rsa.PrivateKey:
keyBytes = x509.MarshalPKCS1PrivateKey(k)
case *ecdsa.PrivateKey:
var err error
keyBytes, err = x509.MarshalECPrivateKey(k)
if err != nil {
return nil, fmt.Errorf("tls client priv key: %v", err)
}
default:
return nil, errors.New("tls client priv key: unsupported key type")
}
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: %v", 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: %v", err)
}
caCertPool := basePool(opts.LoadedCAPool)
caCertPool.AppendCertsFromPEM(caCert)
cfg.RootCAs = caCertPool
case opts.LoadedCAPool != nil:
cfg.RootCAs = opts.LoadedCAPool
}
// apply servername overrride
if opts.ServerName != "" {
cfg.InsecureSkipVerify = false
cfg.ServerName = opts.ServerName
}
return cfg, nil
}
// TLSTransport creates a [http] 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
}
// Runtime represents an API client that uses the transport
// to make [http] requests based on a swagger specification.
type Runtime struct {
@@ -228,17 +44,50 @@ type Runtime struct {
Host string
BasePath string
Formats strfmt.Registry
Context context.Context //nolint:containedctx // we precisely want this type to contain the request context
// Deprecated: prefer [runtime.ContextualTransport.SubmitContext] to pass the request context explicitly.
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
Debug 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
}
var _ runtime.ContextualTransport = &Runtime{}
// New creates a new default runtime for a swagger api runtime.Client.
func New(host, basePath string, schemes []string) *Runtime {
var rt Runtime
@@ -246,13 +95,15 @@ func New(host, basePath string, schemes []string) *Runtime {
// 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(),
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.MultipartFormMime: runtime.ByteStreamConsumer(),
runtime.URLencodedFormMime: runtime.ByteStreamConsumer(),
runtime.DefaultMime: runtime.ByteStreamConsumer(),
}
rt.Producers = map[string]runtime.Producer{
runtime.YAMLMime: yamlpc.YAMLProducer(),
@@ -294,47 +145,6 @@ func NewWithClient(host, basePath string, schemes []string, client *http.Client)
return rt
}
// 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...)
}
// 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)
}
// EnableConnectionReuse drains the remaining body from a response
// so that go will reuse the TCP connections.
//
@@ -357,105 +167,109 @@ func (r *Runtime) EnableConnectionReuse() {
)
}
// 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.createHttpRequest(operation)
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) {
_, readResponse, _ := operation.Params, operation.Reader, operation.AuthInfo
return r.SubmitContext(r.ensureContext(operation), operation)
}
request, req, err := r.createHttpRequest(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
}
r.clientOnce.Do(func() {
r.client = &http.Client{
Transport: r.Transport,
Jar: r.Jar,
}
})
if r.Debug {
b, err2 := httputil.DumpRequestOut(req, true)
if err2 != nil {
return nil, err2
}
r.logger.Debugf("%s\n", string(b))
}
var parentCtx context.Context
switch {
case operation.Context != nil:
parentCtx = operation.Context
case r.Context != nil:
parentCtx = r.Context
default:
parentCtx = context.Background()
}
var (
ctx context.Context
cancel context.CancelFunc
)
if request.timeout == 0 {
// There may be a deadline in the context passed to the operation.
// Otherwise, there is no timeout set.
ctx, cancel = context.WithCancel(parentCtx)
} else {
// Sets the timeout passed from request params (by default runtime.DefaultTimeout).
// If there is already a deadline in the parent context, the shortest will
// apply.
ctx, cancel = context.WithTimeout(parentCtx, request.timeout)
}
defer cancel()
var client *http.Client
if operation.Client != nil {
client = operation.Client
} else {
client = r.client
r.ensureClient()
if err := r.dumpRequest(req); err != nil {
return nil, err
}
req = req.WithContext(ctx)
res, err := client.Do(req) // make requests, by default follows 10 redirects before failing
// 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 r.Debug {
printBody := true
if ct == runtime.DefaultMime {
printBody = false // Spare the terminal from a binary blob.
}
b, err2 := httputil.DumpResponse(res, printBody)
if err2 != nil {
return nil, err2
}
r.logger.Debugf("%s\n", string(b))
if err := r.dumpResponse(res, ct); err != nil {
return nil, err
}
mt, _, err := mime.ParseMediaType(ct)
cons, err := r.resolveConsumer(ct)
if err != nil {
return nil, fmt.Errorf("parse content type: %s", err)
return nil, err
}
cons, ok := r.Consumers[mt]
if !ok {
if cons, ok = r.Consumers["*/*"]; !ok {
// scream about not knowing what to do
return nil, fmt.Errorf("no consumer: %q", ct)
}
}
return readResponse.ReadResponse(r.response(res), cons)
return operation.Reader.ReadResponse(r.response(res), cons)
}
// SetDebug changes the debug flag.
@@ -482,6 +296,17 @@ func (r *Runtime) SetResponseReader(f ClientResponseFunc) {
r.response = f
}
func (r *Runtime) ensureContext(operation *runtime.ClientOperation) context.Context {
switch {
case operation.Context != nil: //nolint:staticcheck // kept for backward compatibility
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
@@ -518,16 +343,121 @@ func transportOrDefault(left, right http.RoundTripper) http.RoundTripper {
return left
}
// takes a client operation and creates equivalent http.Request.
func (r *Runtime) createHttpRequest(operation *runtime.ClientOperation) (*request, *http.Request, error) { //nolint:revive
// 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
request := newRequest(operation.Method, operation.PathPattern, params)
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 := request.SetHeaderParam(runtime.HeaderAccept, accept...); err != nil {
return nil, nil, err
if err := req.SetHeaderParam(runtime.HeaderAccept, accept...); err != nil {
return nil, "", nil, err
}
if auth == nil && r.DefaultAuthentication != nil {
@@ -538,39 +468,75 @@ func (r *Runtime) createHttpRequest(operation *runtime.ClientOperation) (*reques
return r.DefaultAuthentication.AuthenticateRequest(req, reg)
})
}
// if auth != nil {
// if err := auth.AuthenticateRequest(request, r.Formats); err != nil {
// return nil, err
// }
//}
// Enhancement proposal: https://github.com/go-openapi/runtime/issues/386
cmt := r.DefaultMediaType
for _, mediaType := range operation.ConsumesMediaTypes {
// Pick first non-empty media type
if mediaType != "" {
cmt = mediaType
break
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
}
}
if _, ok := r.Producers[cmt]; !ok && cmt != runtime.MultipartFormMime && cmt != runtime.URLencodedFormMime {
return nil, nil, fmt.Errorf("none of producers: %v registered. try %s", r.Producers, cmt)
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
}
}
req, err := request.buildHTTP(cmt, r.BasePath, r.Producers, r.Formats, auth)
if err != nil {
return nil, nil, err
if firstNonEmpty != "" {
return firstNonEmpty
}
req.URL.Scheme = r.pickScheme(operation.Schemes)
req.URL.Host = r.Host
req.Host = r.Host
return request, req, nil
return def
}
func basePool(pool *x509.CertPool) *x509.CertPool {
if pool == nil {
return x509.NewCertPool()
}
return pool
// 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)
}
+197
View File
@@ -0,0 +1,197 @@
// 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()
}
+21 -3
View File
@@ -19,12 +19,30 @@ type ClientOperation struct {
AuthInfo ClientAuthInfoWriter
Params ClientRequestWriter
Reader ClientResponseReader
Context context.Context //nolint:containedctx // we precisely want this type to contain the request context
Client *http.Client
// Deprecated: prefer [ContextualTransport.SubmitContext] to pass the request context explicitly.
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 the operation and return the deserialized response or an error.
Submit(*ClientOperation) (any, error)
}
// ContextualTransport extends [ClientTransport] with an explicit
// context-aware submission method.
//
// Wrappers such as the OpenTelemetry transport type-assert to this
// interface so they can forward an explicit context to the underlying
// transport without setting the cached [ClientOperation.Context] field.
//
// In v2, SubmitContext will be folded into [ClientTransport] itself
// and the cached [ClientOperation.Context] field removed; this interface
// is the v0.x bridge.
type ContextualTransport interface {
ClientTransport
// SubmitContext submits the operation using ctx as the request context.
SubmitContext(ctx context.Context, operation *ClientOperation) (any, error)
}
+1 -1
View File
@@ -59,7 +59,7 @@ func (o *APIError) Error() string {
if err, ok := o.Response.(error); ok {
resp = []byte("'" + sanitizer.Replace(err.Error()) + "'")
} else {
resp, _ = json.Marshal(o.Response)
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)
+6 -2
View File
@@ -21,8 +21,12 @@ const (
DefaultMime = "application/octet-stream"
// JSONMime the json mime type.
JSONMime = "application/json"
// YAMLMime the [yaml] mime type.
YAMLMime = "application/x-yaml"
// 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.
+9 -9
View File
@@ -100,7 +100,7 @@ func CSVConsumer(opts ...CSVOpt) Consumer {
default:
// support *[][]string, *[]byte, *string
if ptr := reflect.TypeOf(data); ptr.Kind() != reflect.Ptr {
if ptr := reflect.TypeOf(data); ptr.Kind() != reflect.Pointer {
return errors.New("destination must be a pointer")
}
@@ -159,14 +159,14 @@ func CSVConsumer(opts ...CSVOpt) Consumer {
//
// 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
// - *[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 {
+6
View File
@@ -5,4 +5,10 @@ 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
+355
View File
@@ -0,0 +1,355 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package runtime
import (
stderrors "errors"
"fmt"
"mime/multipart"
"net/http"
"strings"
"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
}
// FormFile resolves a file field from a parsed form body, transparently
// handling both content types accepted for `type: file` parameters by
// the OpenAPI 2.0 spec:
//
// - multipart/form-data — delegates to [http.Request.FormFile].
// - application/x-www-form-urlencoded — looks up the field in
// r.PostForm and synthesizes a [multipart.File] backed by the
// value bytes plus a [multipart.FileHeader] with Filename equal
// to the field name and Size set to the byte length.
//
// Returns [http.ErrMissingFile] when the field is absent under either
// content type. Callers must have parsed the body upstream (e.g. via
// [BindForm] or [http.Request.ParseForm]) before reading from the
// urlencoded path — [http.Request.FormFile] takes care of parsing on
// the multipart path.
//
// Presence is the only criterion for binding a urlencoded file: an
// empty value (e.g. `file=`) is bound as a zero-byte file.
func FormFile(r *http.Request, name string) (multipart.File, *multipart.FileHeader, error) {
file, header, err := r.FormFile(name)
if err == nil {
return file, header, nil
}
if !stderrors.Is(err, http.ErrNotMultipart) {
return nil, nil, err
}
values, present := r.PostForm[name]
if !present {
return nil, nil, http.ErrMissingFile
}
value := values[0]
return urlencodedFile{Reader: strings.NewReader(value)},
&multipart.FileHeader{Filename: name, Size: int64(len(value))},
nil
}
// urlencodedFile adapts a urlencoded form value (already buffered in
// memory by [http.Request.ParseForm]) to the [multipart.File]
// interface. The embedded [strings.Reader] supplies Read/ReadAt/Seek;
// Close is a no-op since there is no resource to release.
type urlencodedFile struct {
*strings.Reader
}
func (urlencodedFile) Close() error { return nil }
func bindFormFile(r *http.Request, spec formFileSpec, maxFilenameLen int) error {
file, header, err := FormFile(r, 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)
}
+3 -1
View File
@@ -1,6 +1,8 @@
use (
.
./client-middleware/opentracing
./docs/examples
./server-middleware
)
go 1.24.0
go 1.25.0
-109
View File
@@ -1,109 +0,0 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw=
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+22
View File
@@ -99,3 +99,25 @@ type Validatable interface {
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
}
+418 -316
View File
@@ -5,10 +5,9 @@ package middleware
import (
stdContext "context"
stderrors "errors"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"sync"
@@ -17,19 +16,21 @@ import (
"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.
//
// (Note: The correct spelling is "library", not "libra". "Libra" is a zodiac sign/constellation and wouldn't make sense in this context.)
var Logger logger.Logger = logger.StandardLogger{}
func debugLogfFunc(lg logger.Logger) func(string, ...any) {
@@ -75,121 +76,13 @@ func (fn ResponderFunc) WriteResponse(rw http.ResponseWriter, pr runtime.Produce
// 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
}
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)
if oh, ok := api.OperationHandlerFor(method, path); ok {
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
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.
@@ -246,6 +139,156 @@ 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 {
@@ -319,57 +362,42 @@ func (c *Context) RequiredProduces() []string {
// 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 res []error
var requestContentType string
// check and validate content type, select consumer
if runtime.HasBody(request) {
ct, _, err := runtime.ContentType(request.Header)
ct, cons, err := c.bindRequestBody(request, route)
if err != nil {
res = append(res, err)
} else {
c.debugLogf("validating content type for %q against [%s]", ct, strings.Join(route.Consumes, ", "))
if err := validateContentType(route.Consumes, ct); err != nil {
res = append(res, err)
}
if len(res) == 0 {
cons, ok := route.Consumers[ct]
if !ok {
res = append(res, errors.New(http.StatusInternalServerError, "no consumer registered for %s", ct))
} else {
route.Consumer = cons
requestContentType = ct
}
}
return errors.CompositeValidationError(err)
}
// happy path
requestContentType = ct
route.Consumer = cons
}
// check and validate the response format
if len(res) == 0 {
// 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 = "*/*"
}
// 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 = "*/*"
}
if str := NegotiateContentType(request, route.Produces, requestContentType); str == "" {
res = append(res, errors.InvalidResponseFormat(request.Header.Get(runtime.HeaderAccept), route.Produces))
}
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
if binder != nil && len(res) == 0 {
if err := binder.BindRequest(request, route); err != nil {
return err
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
return binder.BindRequest(request, route)
}
// ContentType gets the parsed value of a content type
@@ -431,7 +459,7 @@ func (c *Context) ResponseFormat(r *http.Request, offers []string) (string, *htt
return v, r
}
format := NegotiateContentType(r, offers, "")
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))
@@ -453,44 +481,6 @@ func (c *Context) ResetAuth(request *http.Request) *http.Request {
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() && usr == nil {
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 {
if _, ok := err.(errors.Error); ok {
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
@@ -523,91 +513,29 @@ func (c *Context) NotFound(rw http.ResponseWriter, r *http.Request) {
// 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 := []string{}
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)
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 {
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)
c.respondWithResponder(rw, r, route, resp, format)
return
}
if err, ok := data.(error); ok {
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)
c.respondWithError(rw, r, produces, route, err, format)
return
}
if route == nil || route.Operation == nil {
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
}
c.respondWithoutCode(rw, r, data, format, offers)
return
}
if _, code, ok := route.Operation.SuccessResponse(); ok {
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
}
c.respondWithCode(rw, r, route, code, data, format)
return
}
@@ -618,57 +546,76 @@ func (c *Context) Respond(rw http.ResponseWriter, r *http.Request, produces []st
//
// This handler includes a swagger spec, router and the contract defined in the swagger spec.
//
// A spec UI ([SwaggerUI]) is served at {API base path}/docs and the spec document at /swagger.json
// (these can be modified with uiOptions).
// 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 {
b := builder
if b == nil {
b = PassthroughBuilder
}
specPath, uiOpts, specOpts := c.uiOptionsForHandler(opts)
var swaggerUIOpts SwaggerUIOpts
fromCommonToAnyOptions(uiOpts, &swaggerUIOpts)
return Spec(specPath, c.spec.Raw(), SwaggerUI(swaggerUIOpts, c.RoutesHandler(b)), specOpts...)
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 ([RapiDoc]) is served at {API base path}/docs and the spec document at /swagger.json
// (these can be modified with uiOptions).
// 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 {
b := builder
if b == nil {
b = PassthroughBuilder
}
specPath, uiOpts, specOpts := c.uiOptionsForHandler(opts)
var rapidocUIOpts RapiDocOpts
fromCommonToAnyOptions(uiOpts, &rapidocUIOpts)
return Spec(specPath, c.spec.Raw(), RapiDoc(rapidocUIOpts, c.RoutesHandler(b)), specOpts...)
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 ([Redoc]) is served at {API base path}/docs and the spec document at /swagger.json
// (these can be modified with uiOptions).
// 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
}
specPath, uiOpts, specOpts := c.uiOptionsForHandler(opts)
var redocOpts RedocOpts
fromCommonToAnyOptions(uiOpts, &redocOpts)
// the UI titles defaults to the title in the spec
const extraOptions = 2
prepend := make([]docui.Option, 0, len(opts)+extraOptions)
var title string
return Spec(specPath, c.spec.Raw(), Redoc(redocOpts, c.RoutesHandler(b)), specOpts...)
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.
@@ -680,37 +627,192 @@ func (c *Context) RoutesHandler(builder Builder) http.Handler {
return NewRouter(c, b(NewOperationExecutor(c)))
}
func (c Context) uiOptionsForHandler(opts []UIOption) (string, uiOptions, []SpecOption) {
var title string
sp := c.spec.Spec()
if sp != nil && sp.Info != nil && sp.Info.Title != "" {
title = sp.Info.Title
// authorizeImpl is the real authentication+authorization body shared
// between the production and dev-only variants of [Context.Authorize].
// See context_skipauth_disabled.go (default build) and
// context_skipauth_enabled.go (the `openapi_unsafe_skipauth` build tag).
//
// The doc on the exported Authorize describes the user-facing
// contract; this function MUST NOT change semantics for the
// production path.
func (c *Context) authorizeImpl(request *http.Request, route *MatchedRoute) (any, *http.Request, error) {
if route == nil || !route.HasAuth() {
return nil, nil, nil
}
// default options (may be overridden)
const baseOptions = 2
optsForContext := make([]UIOption, 0, len(opts)+baseOptions)
optsForContext = append(optsForContext,
WithUIBasePath(c.BasePath()),
WithUITitle(title),
)
optsForContext = append(optsForContext, opts...)
uiOpts := uiOptionsWithDefaults(optsForContext)
// If 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)
var specPath string
if u != nil {
specPath = u.Path
var rCtx = request.Context()
if v := rCtx.Value(ctxSecurityPrincipal); v != nil {
return v, request, nil
}
pth, doc := path.Split(specPath)
if pth == "." {
pth = ""
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)
}
}
return pth, uiOpts, []SpecOption{WithSpecDocument(doc)}
rCtx = request.Context()
rCtx = stdContext.WithValue(rCtx, ctxSecurityPrincipal, usr)
rCtx = stdContext.WithValue(rCtx, ctxSecurityScopes, route.Authenticator.AllScopes())
return usr, request.WithContext(rCtx), nil
}
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 {
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
//go:build !openapi_unsafe_skipauth
package middleware
import "net/http"
// 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.
//
// This is the production variant — compiled when the build tag
// `openapi_unsafe_skipauth` is NOT set. There is no skip-auth check
// in this codepath; the field, setter, and storage for the bypass
// flag are entirely absent from the binary. See the alternate
// implementation in context_skipauth_enabled.go for the dev-only
// bypass mechanism.
func (c *Context) Authorize(request *http.Request, route *MatchedRoute) (any, *http.Request, error) {
return c.authorizeImpl(request, route)
}
@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
//go:build openapi_unsafe_skipauth
package middleware
import (
"log"
"net/http"
"sync/atomic"
)
// skipAuthEnabled holds the process-wide skip-auth flag. It only
// exists in binaries built with the `openapi_unsafe_skipauth` tag —
// production binaries (built without the tag) have no field, no
// setter, no storage, and no skip-checking branch in [Context.Authorize].
// Reflection, unsafe-pointer arithmetic, or a debugger cannot flip
// what is not in the binary.
var skipAuthEnabled atomic.Bool
// SetSkipAuth toggles a PROCESS-WIDE bypass of authentication AND
// authorization for every operation served by every Context in the
// running program.
//
// DANGER: this disables ALL authentication and ALL authorization.
// Every request to every secured endpoint runs as if it had been
// authorized with a nil principal. Use ONLY on developer
// workstations during early prototyping (e.g. while
// authentication is not yet wired up).
//
// This function exists only when the build tag
// `openapi_unsafe_skipauth` is set:
//
// go build -tags openapi_unsafe_skipauth ./...
//
// Production CI MUST NOT pass this tag. Calls compile to a symbol
// that does not exist in production binaries.
//
// Calling with true emits a one-line WARNING via the stdlib `log`
// package (stderr by default) so the bypass is visible at startup.
// Calling with false silently disables it.
func SetSkipAuth(skip bool) {
skipAuthEnabled.Store(skip)
if skip {
log.Println("WARNING: go-openapi/runtime: SetSkipAuth(true) — authentication and authorization are bypassed for ALL operations. This MUST NOT run in production.")
}
}
// Authorize is the dev-build variant of the production
// [Context.Authorize] (see context_skipauth_disabled.go for the
// production path). When [SetSkipAuth] has enabled the bypass, this
// returns a nil principal with the original request and no error —
// handlers downstream receive a nil-value principal. Otherwise it
// delegates to the standard authentication+authorization body.
func (c *Context) Authorize(request *http.Request, route *MatchedRoute) (any, *http.Request, error) {
if skipAuthEnabled.Load() {
return nil, request, nil
}
return c.authorizeImpl(request, route)
}
+12 -7
View File
@@ -9,6 +9,7 @@ package denco
import (
"errors"
"fmt"
"slices"
"sort"
"strings"
)
@@ -29,8 +30,8 @@ const (
// PathParamCharacter indicates a RESTCONF path param.
PathParamCharacter = '='
// MaxSize is max size of records and internal slice.
MaxSize = (1 << 22) - 1 //nolint:mnd
// MaxSize is the maximum size of records and internal slice (encoded over 22 bits).
MaxSize = (1 << baseBits) - 1
)
// Router represents a URL router.
@@ -53,9 +54,12 @@ func New() *Router {
}
}
// Lookup returns data and path parameters that associated with path.
// 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"}].
//
// 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
@@ -144,6 +148,7 @@ func newDoubleArray() *doubleArray {
type baseCheck uint32
const (
baseBits = 22
flagsBits = 10
checkBits = 8
)
@@ -157,7 +162,7 @@ func (bc *baseCheck) SetBase(base int) {
}
func (bc baseCheck) Check() byte {
return byte(bc) //nolint:gosec // integer conversion is ok
return byte(bc) //nolint:gosec // integer conversion is ok: we pick the last 8 bits
}
func (bc *baseCheck) SetCheck(check byte) {
@@ -213,8 +218,8 @@ func (da *doubleArray) lookup(path string, params []Param, idx int) (*node, []Pa
}
BACKTRACKING:
for j := len(indices) - 1; j >= 0; j-- {
i, idx := int(indices[j]>>indexOffset), int(indices[j]&indexMask)
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) {
+14 -12
View File
@@ -9,7 +9,7 @@ import (
"net/http"
)
// Mux represents a multiplexer for HTTP request.
// Mux represents a multiplexer for HTTP requests.
type Mux struct{}
// NewMux returns a new [Mux].
@@ -17,27 +17,27 @@ func NewMux() *Mux {
return &Mux{}
}
// GET is shorthand of [Mux].Handler("GET", path, handler).
// 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 of [Mux].Handler("POST", 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 of [Mux].Handler("PUT", 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 of [Mux].Handler("HEAD", 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 HTTP method.
// Handler returns a [Handler] for a HTTP method.
func (m *Mux) Handler(method, path string, handler HandlerFunc) Handler {
return Handler{
Method: method,
@@ -63,7 +63,7 @@ func (m *Mux) Build(handlers []Handler) (http.Handler, error) {
return mux, nil
}
// Handler represents a handler of HTTP request.
// Handler represents a handler of HTTP requests.
type Handler struct {
// Method is an HTTP method.
Method string
@@ -75,7 +75,7 @@ type Handler struct {
Func HandlerFunc
}
// HandlerFunc is aliased to type of handler function.
// 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 {
@@ -88,7 +88,7 @@ func newServeMux() *serveMux {
}
}
// ServeHTTP implements http.Handler interface.
// 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)
@@ -97,15 +97,17 @@ func (mux *serveMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
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
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 method or a handler not found.
// If you want to use the your own NotFound handler, please overwrite this variable.
//
// 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)
}
-102
View File
@@ -1,102 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
// Copyright 2013 The Go Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd.
// this file was taken from the github.com/golang/gddo repository
package middleware
import (
"net/http"
"strings"
"github.com/go-openapi/runtime/middleware/header"
)
// NegotiateContentEncoding returns the best offered content encoding for the
// request's Accept-Encoding header. If two offers match with equal weight and
// then the offer earlier in the list is preferred. If no offers are
// acceptable, then "" is returned.
func NegotiateContentEncoding(r *http.Request, offers []string) string {
bestOffer := "identity"
bestQ := -1.0
specs := header.ParseAccept(r.Header, "Accept-Encoding")
for _, offer := range offers {
for _, spec := range specs {
if spec.Q > bestQ &&
(spec.Value == "*" || spec.Value == offer) {
bestQ = spec.Q
bestOffer = offer
}
}
}
if bestQ == 0 {
bestOffer = ""
}
return bestOffer
}
// NegotiateContentType returns the best offered content type for the request's
// Accept header. If two offers match with equal weight, then the more specific
// offer is preferred. For example, text/* trumps */*. If two offers match
// with equal weight and specificity, then the offer earlier in the list is
// preferred. If no offers match, then defaultOffer is returned.
func NegotiateContentType(r *http.Request, offers []string, defaultOffer string) string {
bestOffer := defaultOffer
bestQ := -1.0
bestWild := 3
specs := header.ParseAccept(r.Header, "Accept")
for _, rawOffer := range offers {
offer := normalizeOffer(rawOffer)
// No Accept header: just return the first offer.
if len(specs) == 0 {
return rawOffer
}
for _, spec := range specs {
switch {
case spec.Q == 0.0:
// ignore
case spec.Q < bestQ:
// better match found
case spec.Value == "*/*":
if spec.Q > bestQ || bestWild > 2 {
bestQ = spec.Q
bestWild = 2
bestOffer = rawOffer
}
case strings.HasSuffix(spec.Value, "/*"):
if strings.HasPrefix(offer, spec.Value[:len(spec.Value)-1]) &&
(spec.Q > bestQ || bestWild > 1) {
bestQ = spec.Q
bestWild = 1
bestOffer = rawOffer
}
default:
if spec.Value == offer &&
(spec.Q > bestQ || bestWild > 0) {
bestQ = spec.Q
bestWild = 0
bestOffer = rawOffer
}
}
}
}
return bestOffer
}
func normalizeOffers(orig []string) (norm []string) {
for _, o := range orig {
norm = append(norm, normalizeOffer(o))
}
return
}
func normalizeOffer(orig string) string {
const maxParts = 2
return strings.SplitN(orig, ";", maxParts)[0]
}
+181 -123
View File
@@ -6,7 +6,7 @@ package middleware
import (
"encoding"
"encoding/base64"
"fmt"
stderrors "errors"
"io"
"net/http"
"reflect"
@@ -56,126 +56,153 @@ func (p *untypedParamBinder) Type() reflect.Type {
}
func (p *untypedParamBinder) Bind(request *http.Request, routeParams RouteParams, consumer runtime.Consumer, target reflect.Value) error {
// fmt.Println("binding", p.name, "as", p.Type())
switch p.parameter.In {
case "query":
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)
return p.bindQuery(request, routeParams, consumer, target)
case "header":
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)
return p.bindHeader(request, routeParams, consumer, target)
case "path":
data, custom, hasKey, err := p.readValue(routeParams, target)
if err != nil {
return err
}
if custom {
return nil
}
return p.bindValue(data, hasKey, target)
return p.bindPath(request, routeParams, consumer, target)
case "formData":
var err error
var mt string
return p.bindFormData(request, routeParams, consumer, target)
mt, _, e := runtime.ContentType(request.Header)
if e != nil {
// because of the interface conversion go thinks the error is not nil
// so we first check for nil and then set the err var if it's not nil
err = e
}
case "body":
return p.bindBody(request, routeParams, consumer, target)
default:
return errors.New(http.StatusInternalServerError, "invalid parameter location: %q", p.parameter.In)
}
}
if err != nil {
return errors.InvalidContentType("", []string{"multipart/form-data", "application/x-www-form-urlencoded"})
}
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
}
if mt != "multipart/form-data" && mt != "application/x-www-form-urlencoded" {
return errors.InvalidContentType(mt, []string{"multipart/form-data", "application/x-www-form-urlencoded"})
}
return p.bindValue(data, hasKey, target)
}
if mt == "multipart/form-data" {
if err = request.ParseMultipartForm(defaultMaxMemory); err != nil {
return errors.NewParseError(p.Name, p.parameter.In, "", err)
}
}
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)
}
if err = request.ParseForm(); err != nil {
return errors.NewParseError(p.Name, p.parameter.In, "", err)
}
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)
}
if p.parameter.Type == "file" {
file, header, ffErr := request.FormFile(p.parameter.Name)
if ffErr != nil {
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" {
// runtime.FormFile handles both multipart/form-data and
// application/x-www-form-urlencoded (OpenAPI 2.0 permits
// either consumes for `type: file`), and surfaces a
// missing field as http.ErrMissingFile under both.
file, header, ffErr := runtime.FormFile(request, p.parameter.Name)
if ffErr != nil {
if stderrors.Is(ffErr, http.ErrMissingFile) {
if p.parameter.Required {
return errors.NewParseError(p.Name, p.parameter.In, "", ffErr)
return errors.NewParseError(p.Name, p.parameter.In, "", http.ErrMissingFile)
}
return nil
}
target.Set(reflect.ValueOf(runtime.File{Data: file, Header: header}))
return nil
return errors.NewParseError(p.Name, p.parameter.In, "", ffErr)
}
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 {
// 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)
}
case "body":
newValue := reflect.New(target.Type())
if !runtime.HasBody(request) {
if p.parameter.Default != nil {
target.Set(reflect.ValueOf(p.parameter.Default))
}
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
}
if err := consumer.Consume(request.Body, newValue.Interface()); err != nil {
if 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)
tpe := p.parameter.Type
if p.parameter.Format != "" {
tpe = p.parameter.Format
}
target.Set(reflect.Indirect(newValue))
return nil
default:
return fmt.Errorf("%d: invalid parameter location %q", http.StatusInternalServerError, p.parameter.In)
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 {
@@ -261,20 +288,51 @@ func (p *untypedParamBinder) bindValue(data []string, hasKey bool, target reflec
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) setFieldValue(target reflect.Value, defaultValue any, data string, hasKey bool) error { //nolint:gocyclo
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 (!hasKey || (!p.parameter.AllowEmptyValue && data == "")) && p.parameter.Required && p.parameter.Default == nil {
if p.isMissingAndRequired(hasKey, data) {
return errors.Required(p.Name, p.parameter.In, data)
}
@@ -292,27 +350,15 @@ func (p *untypedParamBinder) setFieldValue(target reflect.Value, defaultValue an
}
if tpe == "byte" {
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
return p.setByte(target, defVal, tpe, data)
}
switch target.Kind() { //nolint:exhaustive // we want to check only types that map from a swagger parameter
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() {
@@ -327,6 +373,7 @@ func (p *untypedParamBinder) setFieldValue(target reflect.Value, defaultValue an
if target.CanSet() {
target.SetBool(b)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if data == "" {
if target.CanSet() {
@@ -394,8 +441,8 @@ func (p *untypedParamBinder) setFieldValue(target reflect.Value, defaultValue an
target.SetString(value)
}
case reflect.Ptr:
if data == "" && defVal.Kind() == reflect.Ptr {
case reflect.Pointer:
if data == "" && defVal.Kind() == reflect.Pointer {
if target.CanSet() {
target.Set(defVal)
}
@@ -412,6 +459,7 @@ func (p *untypedParamBinder) setFieldValue(target reflect.Value, defaultValue an
default:
return errors.InvalidType(p.Name, p.parameter.In, tpe, data)
}
return nil
}
@@ -419,20 +467,30 @@ func (p *untypedParamBinder) tryUnmarshaler(target reflect.Value, defaultValue a
if !target.CanSet() {
return false, nil
}
// When a type implements encoding.TextUnmarshaler we'll use that instead of reflecting some more
if reflect.PointerTo(target.Type()).Implements(textUnmarshalType) {
if defaultValue != nil && len(data) == 0 {
target.Set(reflect.ValueOf(defaultValue))
return true, nil
}
value := reflect.New(target.Type())
if err := value.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(data)); err != nil {
return true, err
}
target.Set(reflect.Indirect(value))
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
}
return false, 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) {
-83
View File
@@ -1,83 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"bytes"
"fmt"
"html/template"
"net/http"
"path"
)
// RapiDocOpts configures the [RapiDoc] middlewares.
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 (r *RapiDocOpts) EnsureDefaults() {
common := toCommonUIOptions(r)
common.EnsureDefaults()
fromCommonToAnyOptions(common, r)
// rapidoc-specifics
if r.RapiDocURL == "" {
r.RapiDocURL = rapidocLatest
}
if r.Template == "" {
r.Template = rapidocTemplate
}
}
// RapiDoc creates a [middleware] to serve a documentation site for a swagger spec.
//
// This allows for altering the spec before starting the [http] listener.
func RapiDoc(opts RapiDocOpts, next http.Handler) http.Handler {
opts.EnsureDefaults()
pth := path.Join(opts.BasePath, opts.Path)
tmpl := template.Must(template.New("rapidoc").Parse(opts.Template))
assets := bytes.NewBuffer(nil)
if err := tmpl.Execute(assets, opts); err != nil {
panic(fmt.Errorf("cannot execute template: %w", err))
}
return serveUI(pth, assets.Bytes(), next)
}
const (
rapidocLatest = "https://unpkg.com/rapidoc/dist/rapidoc-min.js"
rapidocTemplate = `<!doctype html>
<html>
<head>
<title>{{ .Title }}</title>
<meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 characters -->
<script type="module" src="{{ .RapiDocURL }}"></script>
</head>
<body>
<rapi-doc spec-url="{{ .SpecURL }}"></rapi-doc>
</body>
</html>
`
)
-97
View File
@@ -1,97 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"bytes"
"fmt"
"html/template"
"net/http"
"path"
)
// RedocOpts configures the [Redoc] middlewares.
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
}
// EnsureDefaults in case some options are missing.
func (r *RedocOpts) EnsureDefaults() {
common := toCommonUIOptions(r)
common.EnsureDefaults()
fromCommonToAnyOptions(common, r)
// redoc-specifics
if r.RedocURL == "" {
r.RedocURL = redocLatest
}
if r.Template == "" {
r.Template = redocTemplate
}
}
// Redoc creates a [middleware] to serve a documentation site for a swagger spec.
//
// This allows for altering the spec before starting the [http] listener.
func Redoc(opts RedocOpts, next http.Handler) http.Handler {
opts.EnsureDefaults()
pth := path.Join(opts.BasePath, opts.Path)
tmpl := template.Must(template.New("redoc").Parse(opts.Template))
assets := bytes.NewBuffer(nil)
if err := tmpl.Execute(assets, opts); err != nil {
panic(fmt.Errorf("cannot execute template: %w", err))
}
return serveUI(pth, assets.Bytes(), next)
}
const (
redocLatest = "https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"
redocTemplate = `<!DOCTYPE html>
<html>
<head>
<title>{{ .Title }}</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='{{ .SpecURL }}'></redoc>
<script src="{{ .RedocURL }}"> </script>
</body>
</html>
`
)
+17 -7
View File
@@ -40,8 +40,25 @@ func NewUntypedRequestBinder(parameters map[string]spec.Parameter, spec *spec.Sw
// 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 {
@@ -94,13 +111,6 @@ func (o *UntypedRequestBinder) Bind(request *http.Request, routeParams RoutePara
return nil
}
// 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) setDebugLogf(fn func(string, ...any)) {
o.debugLogf = fn
}
+73 -48
View File
@@ -21,6 +21,7 @@ import (
"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.
@@ -292,7 +293,7 @@ func (ras RouteAuthenticators) Authenticate(req *http.Request, route *MatchedRou
continue
}
applies, usr, err := ra.Authenticate(req, route)
if !applies || err != nil || usr == nil {
if !applies || err != nil || typeutils.IsZero(usr) {
if err != nil {
lastError = err
}
@@ -348,49 +349,62 @@ func (m *MatchedRoute) NeedsAuth() bool {
func (d *defaultRouter) Lookup(method, path string) (*MatchedRoute, bool) {
mth := strings.ToUpper(method)
d.debugLogf("looking up route for %s %s", method, path)
if Debug {
if len(d.routers) == 0 {
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)
}
}
if router, ok := d.routers[mth]; ok {
if m, rp, ok := router.Lookup(fpath.Clean(path)); ok && m != nil {
if entry, ok := m.(*routeEntry); ok {
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
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 {
d.debugLogf("couldn't find a route by path for %s %s", method, path)
// use the parameter directly
params = append(params, RouteParam{Name: p.Name, Value: v})
}
} else {
d.debugLogf("couldn't find a route by method for %s %s", method, path)
}
return nil, false
return &MatchedRoute{routeEntry: *entry, Params: params}, true
}
func (d *defaultRouter) OtherMethods(method, path string) []string {
@@ -398,7 +412,7 @@ func (d *defaultRouter) OtherMethods(method, path string) []string {
var methods []string
for k, v := range d.routers {
if k != mn {
if _, _, ok := v.Lookup(fpath.Clean(path)); ok {
if _, _, ok := v.Lookup(fpath.Clean(escapeLiteralColons(path))); ok {
methods = append(methods, k)
continue
}
@@ -414,28 +428,39 @@ func (d *defaultRouter) SetLogger(lg logger.Logger) {
// 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, "")
}
} else {
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)
return names, 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) {
@@ -463,7 +488,7 @@ func (d *defaultRouteBuilder) AddRoute(method, path string, operation *spec.Oper
requestBinder := NewUntypedRequestBinder(parameters, d.spec.Spec(), d.api.Formats())
requestBinder.setDebugLogf(d.debugLogf)
record := denco.NewRecord(pathConverter.ReplaceAllString(path, ":$1"), &routeEntry{
record := denco.NewRecord(pathConverter.ReplaceAllString(escapeLiteralColons(path), ":$1"), &routeEntry{
BasePath: bp,
PathPattern: path,
Operation: operation,
+482
View File
@@ -0,0 +1,482 @@
// 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
}
}
-91
View File
@@ -1,91 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"net/http"
"path"
)
const (
contentTypeHeader = "Content-Type"
applicationJSON = "application/json"
)
// SpecOption can be applied to the Spec serving [middleware].
type SpecOption func(*specOptions)
var defaultSpecOptions = specOptions{
Path: "",
Document: "swagger.json",
}
type specOptions struct {
Path string
Document string
}
func specOptionsWithDefaults(opts []SpecOption) specOptions {
o := defaultSpecOptions
for _, apply := range opts {
apply(&o)
}
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").
func Spec(basePath string, b []byte, next http.Handler, opts ...SpecOption) http.Handler {
if basePath == "" {
basePath = "/"
}
o := specOptionsWithDefaults(opts)
pth := path.Join(basePath, o.Path, o.Document)
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(b)
return
}
if next != nil {
next.ServeHTTP(rw, r)
return
}
rw.Header().Set(contentTypeHeader, applicationJSON)
rw.WriteHeader(http.StatusNotFound)
})
}
// WithSpecPath sets the path to be joined to the base path of the Spec [middleware].
//
// 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
}
}
-178
View File
@@ -1,178 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"bytes"
"fmt"
"html/template"
"net/http"
"path"
)
// SwaggerUIOpts configures the [SwaggerUI] [middleware].
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
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
}
// EnsureDefaults in case some options are missing.
func (r *SwaggerUIOpts) EnsureDefaults() {
r.ensureDefaults()
if r.Template == "" {
r.Template = swaggeruiTemplate
}
}
func (r *SwaggerUIOpts) EnsureDefaultsOauth2() {
r.ensureDefaults()
if r.Template == "" {
r.Template = swaggerOAuthTemplate
}
}
func (r *SwaggerUIOpts) ensureDefaults() {
common := toCommonUIOptions(r)
common.EnsureDefaults()
fromCommonToAnyOptions(common, r)
// swaggerui-specifics
if r.OAuthCallbackURL == "" {
r.OAuthCallbackURL = path.Join(r.BasePath, r.Path, "oauth2-callback")
}
if r.SwaggerURL == "" {
r.SwaggerURL = swaggerLatest
}
if r.SwaggerPresetURL == "" {
r.SwaggerPresetURL = swaggerPresetLatest
}
if r.SwaggerStylesURL == "" {
r.SwaggerStylesURL = swaggerStylesLatest
}
if r.Favicon16 == "" {
r.Favicon16 = swaggerFavicon16Latest
}
if r.Favicon32 == "" {
r.Favicon32 = swaggerFavicon32Latest
}
}
// SwaggerUI creates a [middleware] to serve a documentation site for a swagger spec.
//
// This allows for altering the spec before starting the [http] listener.
func SwaggerUI(opts SwaggerUIOpts, next http.Handler) http.Handler {
opts.EnsureDefaults()
pth := path.Join(opts.BasePath, opts.Path)
tmpl := template.Must(template.New("swaggerui").Parse(opts.Template))
assets := bytes.NewBuffer(nil)
if err := tmpl.Execute(assets, opts); err != nil {
panic(fmt.Errorf("cannot execute template: %w", err))
}
return serveUI(pth, assets.Bytes(), next)
}
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 = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ .Title }}</title>
<link rel="stylesheet" type="text/css" href="{{ .SwaggerStylesURL }}" >
<link rel="icon" type="image/png" href="{{ .Favicon32 }}" sizes="32x32" />
<link rel="icon" type="image/png" href="{{ .Favicon16 }}" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="{{ .SwaggerURL }}"> </script>
<script src="{{ .SwaggerPresetURL }}"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: '{{ .SpecURL }}',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
oauth2RedirectUrl: '{{ .OAuthCallbackURL }}'
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>
`
)
+30
View File
@@ -0,0 +1,30 @@
// 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
}
-176
View File
@@ -1,176 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"bytes"
"encoding/gob"
"fmt"
"net/http"
"path"
"strings"
)
const (
// constants that are common to all UI-serving middlewares.
defaultDocsPath = "docs"
defaultDocsURL = "/swagger.json"
defaultDocsTitle = "API Documentation"
)
// uiOptions defines common options for UI serving middlewares.
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
}
// toCommonUIOptions converts any UI option type to retain the common options.
//
// This uses gob encoding/decoding to convert common fields from one struct to another.
func toCommonUIOptions(opts any) uiOptions {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
dec := gob.NewDecoder(&buf)
var o uiOptions
err := enc.Encode(opts)
if err != nil {
panic(err)
}
err = dec.Decode(&o)
if err != nil {
panic(err)
}
return o
}
func fromCommonToAnyOptions[T any](source uiOptions, target *T) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
dec := gob.NewDecoder(&buf)
err := enc.Encode(source)
if err != nil {
panic(err)
}
err = dec.Decode(target)
if err != nil {
panic(err)
}
}
// UIOption can be applied to UI serving [middleware], such as Context.[APIHandler] or
// Context.[APIHandlerSwaggerUI] to alter the default behavior.
type UIOption func(*uiOptions)
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.
//
// By default, Context [middleware] sets this value to the API base path.
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}.
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".
func WithUISpecURL(specURL string) UIOption {
return func(o *uiOptions) {
o.SpecURL = specURL
}
}
// WithUITitle sets the title of the UI.
//
// By default, Context [middleware] sets this value to the title found in the API spec.
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.
func WithTemplate(tpl string) UIOption {
return func(o *uiOptions) {
o.Template = tpl
}
}
// EnsureDefaults in case some options are missing.
func (r *uiOptions) EnsureDefaults() {
if r.BasePath == "" {
r.BasePath = "/"
}
if r.Path == "" {
r.Path = defaultDocsPath
}
if r.SpecURL == "" {
r.SpecURL = defaultDocsURL
}
if r.Title == "" {
r.Title = defaultDocsTitle
}
}
// serveUI creates a middleware 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)
})
}
+57 -45
View File
@@ -4,13 +4,14 @@
package middleware
import (
"mime"
stderrors "errors"
"net/http"
"strings"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/swag/stringutils"
"github.com/go-openapi/runtime/server-middleware/mediatype"
)
type validation struct {
@@ -21,24 +22,28 @@ type validation struct {
bound map[string]any
}
// ContentType validates the content type of a request.
func validateContentType(allowed []string, actual string) error {
// 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
}
mt, _, err := mime.ParseMediaType(actual)
_, ok, err := mediatype.MatchFirst(allowed, actual, opts...)
if ok {
return nil
}
if err != nil {
return errors.InvalidContentType(actual, allowed)
}
if stringutils.ContainsStringsCI(allowed, mt) {
return nil
}
if stringutils.ContainsStringsCI(allowed, "*/*") {
return nil
}
parts := strings.Split(actual, "/")
if len(parts) == 2 && stringutils.ContainsStringsCI(allowed, parts[0]+"/*") {
return nil
return errors.NewParseError(runtime.HeaderContentType, "header", actual, err)
}
return errors.InvalidContentType(actual, allowed)
}
@@ -69,46 +74,53 @@ func (v *validation) debugLogf(format string, args ...any) {
func (v *validation) parameters() {
v.debugLogf("validating request parameters for %s %s", v.request.Method, v.request.URL.EscapedPath())
if result := v.route.Binder.Bind(v.request, v.route.Params, v.route.Consumer, v.bound); result != nil {
if result.Error() == "validation failure list" {
for _, e := range result.(*errors.Validation).Value.([]any) {
v.result = append(v.result, e.(error))
}
return
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)
}
v.result = append(v.result, result)
}
}
func (v *validation) contentType() {
if len(v.result) == 0 && runtime.HasBody(v.request) {
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 || !runtime.HasBody(v.request) {
return
}
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); err != nil {
v.result = append(v.result, err)
}
}
if ct != "" && v.route.Consumer == nil {
cons, ok := v.route.Consumers[ct]
if !ok {
v.result = append(v.result, errors.New(http.StatusInternalServerError, "no consumer registered for %s", ct))
} else {
v.route.Consumer = cons
}
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 identify then return an error.
// 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 {
+37 -10
View File
@@ -19,8 +19,8 @@ const (
accessTokenParam = "access_token"
)
// HttpAuthenticator is a function that authenticates a HTTP request.
func HttpAuthenticator(handler func(*http.Request) (bool, any, error)) runtime.Authenticator { //nolint:revive
// 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)
@@ -32,7 +32,14 @@ func HttpAuthenticator(handler func(*http.Request) (bool, any, error)) runtime.A
})
}
// ScopedAuthenticator is a function that authenticates a HTTP request against a list of valid scopes.
// 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 {
@@ -42,22 +49,42 @@ func ScopedAuthenticator(handler func(*ScopedAuthRequest) (bool, any, error)) ru
})
}
// UserPassAuthentication authentication function.
// 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 authentication function with [context.Context].
// 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 authentication function.
// 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 authentication function with [context.Context].
// 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 authentication function.
// 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 authentication function with [context.Context].
// 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"
@@ -199,7 +226,7 @@ func APIKeyAuthCtx(name, in string, authenticate TokenAuthenticationCtx) runtime
})
}
// ScopedAuthRequest contains both a [http] request and the required scopes for a particular operation.
// ScopedAuthRequest contains both the [http.Request] and the required scopes for a particular operation.
type ScopedAuthRequest struct {
Request *http.Request
RequiredScopes []string
+202
View File
@@ -0,0 +1,202 @@
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.
+12
View File
@@ -0,0 +1,12 @@
// 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
+253
View File
@@ -0,0 +1,253 @@
// 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
}
@@ -0,0 +1,67 @@
// 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 = `<!doctype html>
<html>
<head>
<title>{{ .Title }}</title>
<meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 characters -->
<script type="module" src="{{ .AssetsURL }}"></script>
</head>
<body>
<rapi-doc spec-url="{{ .SpecURL }}"></rapi-doc>
</body>
</html>
`
)
+82
View File
@@ -0,0 +1,82 @@
// 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 = `<!DOCTYPE html>
<html>
<head>
<title>{{ .Title }}</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='{{ .SpecURL }}'></redoc>
<script src="{{ .AssetsURL }}"> </script>
</body>
</html>
`
)
+33
View File
@@ -0,0 +1,33 @@
// 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)
})
}
+50
View File
@@ -0,0 +1,50 @@
// 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)
})
}
@@ -0,0 +1,138 @@
// 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 = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ .Title }}</title>
{{- if .SwaggerStylesURL }}
<link rel="stylesheet" type="text/css" href="{{ .SwaggerStylesURL }}" />
{{- end }}
{{- if .Favicon32 }}
<link rel="icon" type="image/png" href="{{ .Favicon32 }}" sizes="32x32" />
{{- end }}
{{- if .Favicon16 }}
<link rel="icon" type="image/png" href="{{ .Favicon16 }}" sizes="16x16" />
{{- end }}
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="{{ .AssetsURL }}"> </script>
{{- if .SwaggerPresetURL }}
<script src="{{ .SwaggerPresetURL }}"> </script>
{{- end }}
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: '{{ .SpecURL }}',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
{{- if .OAuth2CallbackURL }}
oauth2RedirectUrl: '{{ .OAuth2CallbackURL }}'
{{- end }}
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>
`
)
@@ -1,30 +1,56 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package middleware
package docui
import (
"bytes"
"fmt"
"net/http"
"path"
"text/template"
)
func SwaggerUIOAuth2Callback(opts SwaggerUIOpts, next http.Handler) http.Handler {
opts.EnsureDefaultsOauth2()
// 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)
pth := opts.OAuthCallbackURL
tmpl := template.Must(template.New("swaggeroauth").Parse(opts.Template))
assets := bytes.NewBuffer(nil)
if err := tmpl.Execute(assets, opts); err != nil {
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 serveUI(pth, assets.Bytes(), next)
return pth, buf.Bytes()
}
const (
swaggerOAuthTemplate = `
const swaggerOAuth2Template = `
<!DOCTYPE html>
<html lang="en">
<head>
@@ -105,4 +131,3 @@ const (
</body>
</html>
`
)
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
// Package mediatype provides a typed value for media types
// defined by RFC 7231 and RFC 2045.
//
// The matching/selection primitives used by both server-side
// validation and Accept-header negotiation.
//
// The package is stdlib-only.
//
// # The matching rule
//
// [MediaType.Matches] is asymmetric. The receiver acts as 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).
//
// - bare type/subtype must agree, with wildcard handling on either
// side ("*/*" matches anything; "type/*" matches any subtype);
// - if the receiver carries no parameters, any constraint is
// accepted regardless of its parameters;
// - 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 the
// constraint does not list.
//
// q-values are NOT considered by [MediaType.Matches] — they are the
// negotiator's concern, handled inside [Set.BestMatch].
package mediatype
@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package mediatype
// Lookup finds the entry in m matching mediaType, with alias-aware
// fallback. It is the canonical seam for codec-map lookups in both
// the client and server runtimes — placing the fallback policy here
// keeps alias definitions (and any future lookup tolerances) in one
// place.
//
// Lookup tries the following, in order, returning the first hit:
//
// 1. mediaType verbatim (fast path for callers that already pass a
// canonical, parameter-free string and store map keys in the
// same form).
// 2. An alias-aware walk against the parsed "type/subtype" form:
// a direct map hit on the parsed key, on its alias canonical
// if any, and finally an O(len(m)) scan returning any map
// entry whose key alias-canonicalizes to the same target.
// Catches both "map keyed by canonical, query uses alias" and
// "map keyed by one alias, query uses another alias of the
// same canonical".
// 3. When [AllowSuffix] is passed in opts: the same alias-aware
// walk against the RFC 6839 structured-syntax suffix base.
// Catches the "spec/traffic divergence" case (request for
// application/vnd.api+json finds a JSON consumer registered
// under application/json). Query-side suffix fold only — no
// map-side suffix folding.
//
// Lookup does NOT fall back to "*/*". Callers that want wildcard
// behavior (the historical resolveConsumer pattern in the client
// runtime) chain that themselves after a Lookup miss — keeping
// wildcard semantics explicit at each call site.
//
// Map keys are expected in canonical "type/subtype" form (no
// parameters). The runtime's default Consumers / Producers maps
// follow this convention.
//
// Returns (zero, false) when:
//
// - m is empty;
// - mediaType fails to parse and is not present verbatim;
// - none of the active steps hits.
//
// The malformed-vs-not-found distinction is intentionally elided:
// codec-lookup callers treat both as the same "no codec" error path.
func Lookup[T any](m map[string]T, mediaType string, opts ...MatchOption) (T, bool) {
var zero T
if len(m) == 0 {
return zero, false
}
o := applyMatchOptions(opts)
// Fast path: raw key (preserves any caller behaviour that stored
// non-canonical strings as map keys, and skips parsing in the
// common already-canonical case).
if v, ok := m[mediaType]; ok {
return v, true
}
mt, err := Parse(mediaType)
if err != nil {
return zero, false
}
key := mt.Type + "/" + mt.Subtype
if v, ok := findByCanonical(m, key); ok {
return v, true
}
if o.allowSuffix && mt.Suffix != "" {
base := mt.Base()
if baseKey := base.Type + "/" + base.Subtype; baseKey != key {
if v, ok := findByCanonical(m, baseKey); ok {
return v, true
}
}
}
return zero, false
}
// findByCanonical returns the first entry in m whose key
// alias-canonicalizes to the same value as target.
//
// Tries direct hits before the O(len(m)) walk:
//
// 1. m[target] — map keyed by the same string.
// 2. m[aliases[target]] — map keyed by the canonical when target
// is an alias.
// 3. Walk m: return any entry where canonical(k) == canonical(target).
// Catches the "map keyed by an alias different from the query
// side" case (e.g. registered under text/yaml, queried as
// application/x-yaml — both canonicalize to application/yaml).
//
// Map size is single-digit for the runtime's codec maps, so the
// walk is negligible.
func findByCanonical[T any](m map[string]T, target string) (T, bool) {
if v, ok := m[target]; ok {
return v, true
}
canonTarget := target
if canon, ok := aliases[target]; ok {
canonTarget = canon
if v, ok := m[canonTarget]; ok {
return v, true
}
}
for k, v := range m {
kCanon := k
if c, ok := aliases[k]; ok {
kCanon = c
}
if kCanon == canonTarget {
return v, true
}
}
var zero T
return zero, false
}
Loaded 100 of 407 files, more files were not shown because too many files have changed in this diff. Show more