Compare commits

..
Author SHA1 Message Date
Paweł Gronowski 7c0f1b261a Merge pull request #7493 from vvoland/update-docker
vendor: github.com/moby/moby master
2026-11-06 02:29:17 +01:00
Paweł Gronowski e863f84b15 vendor: github.com/moby/moby/client v0.7.0-dev (402d0a16f131)
full diff: https://github.com/moby/moby/compare/client/v0.6.2...402d0a16f131

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-11-06 02:23:19 +01:00
Paweł Gronowski e969249cf5 vendor: github.com/moby/moby/api v1.57.0-dev (402d0a16f131)
full diff: https://github.com/moby/moby/compare/api/v1.56.1...402d0a16f131

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-11-06 02:21:44 +01:00
Paweł Gronowski bad50f6bbe Merge pull request #7492 from thaJeztah/bump_capability
vendor: github.com/moby/sys/capability v0.5.0
2026-11-06 01:14:22 +01:00
Sebastiaan van Stijn 89e99a7ee4 Merge pull request #7393 from renovate-bot/renovate/psampaz-go-mod-outdated-0.x
chore(deps): update psampaz/go-mod-outdated docker tag to v0.9.0
2026-11-06 00:37:20 +01:00
Sebastiaan van Stijn e63c04ae28 vendor: github.com/moby/sys/capability v0.5.0
full diff: https://github.com/moby/sys/compare/capability/v0.4.0...capability/v0.5.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-06 00:23:28 +01:00
Sebastiaan van Stijn c3a866740e Merge pull request #7488 from thaJeztah/docs_trixie
docs: update example versions of debian an ubuntu
2026-11-06 00:05:21 +01:00
Sebastiaan van Stijn dfe5bf3073 Merge pull request #7489 from thaJeztah/bump_go127
update to go1.27.3
2026-11-05 23:59:05 +01:00
Sebastiaan van Stijn 85af295d30 update to go1.27.3
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 23:47:28 +01:00
Sebastiaan van Stijn de692998a4 Merge pull request #7490 from thaJeztah/bump_x_sys
vendor: golang.org/x/sys f5cb0306ac0e (fix for go1.27)
2026-11-05 23:46:40 +01:00
Sebastiaan van Stijn 873db34f4b Merge pull request #7491 from thaJeztah/bump_golangci
update to golangci-lint 2.14 (for go1.27)
2026-11-05 23:46:11 +01:00
Sebastiaan van Stijn 66da404208 Merge pull request #7486 from thaJeztah/bump_trixie
update to debian trixie
2026-11-05 23:39:05 +01:00
Sebastiaan van Stijn a0f7df60ba update to debian trixie
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 23:29:15 +01:00
Sebastiaan van Stijn 8fa64e724a vendor: golang.org/x/sys f5cb0306ac0e (fix for go1.27)
includes a fix for https://go.dev/issue/82324

full diff: https://github.com/golang/sys/compare/v0.49.0...f5cb0306ac0e4b93b83ddd53c36d6693af3214e3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 23:23:39 +01:00
Sebastiaan van Stijn e2aefc6e1a docs: update examples for ubuntu LTS versions
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 23:19:42 +01:00
Sebastiaan van Stijn 26a4786788 update to golangci-lint 2.14 (for go1.27)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 23:13:02 +01:00
Sebastiaan van Stijn 8f1203743d fix linting
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 23:12:57 +01:00
Mend Renovate 32a838bf17 chore(deps): update psampaz/go-mod-outdated docker tag to v0.9.0
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-11-05 21:28:50 +00:00
Paweł Gronowski cbb0dabae0 Merge pull request #7485 from thaJeztah/bump_alpine
update to alpine 3.24
2026-11-05 22:27:33 +01:00
Paweł Gronowski d216e46795 Merge pull request #7487 from thaJeztah/update_runners
ci: replace retired macOS 14 runner with macOS 26
2026-11-05 22:27:15 +01:00
Sebastiaan van Stijn 1c6c7ffcc8 docs: update examples to use debian trixie
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 22:24:31 +01:00
Sebastiaan van Stijn 11626f30d0 ci: replace retired macOS 14 runner with macOS 26
GitHub Actions retired macOS 14 runners on November 2, 2026.
Replace macos-14 with macos-26 to continue testing on supported
macOS versions.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 22:12:03 +01:00
Sebastiaan van Stijn 7817ee1c5c update to alpine 3.24
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 22:02:33 +01:00
Sebastiaan van Stijn 6b88c47537 Merge pull request #7480 from thaJeztah/bump_ansiterm
vendor: github.com/Azure/go-ansiterm 51075c6ea6bc
2026-11-05 21:28:03 +01:00
Sebastiaan van Stijn 2b3688febb Merge pull request #7481 from dperny/fix-container-names-fallback
cli/command/formatter: restore fallback for container names with slashes
2026-11-05 20:13:15 +01:00
Sebastiaan van Stijn 28f460abf3 Merge pull request #7483 from renovate-bot/renovate/docker-compose-bin-5.x
chore(deps): update docker/compose-bin docker tag to v5.7.0
2026-11-05 19:48:39 +01:00
Mend Renovate 1f6f73a7be chore(deps): update docker/compose-bin docker tag to v5.7.0
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-11-05 16:52:38 +00:00
Sebastiaan van Stijn f9ad558f5d Merge pull request #7482 from nicksieger/nicksieger/fix-help-run
fix: propagate leading --help to subcommands with a local help flag
2026-11-05 17:49:18 +01:00
Sebastiaan van Stijn dd422f424c Merge pull request #7433 from jsternberg/trace-and-metrics-shutdown
cli-plugins/plugin: add OnFinalize hook for shutting down otel providers
2026-11-05 17:48:40 +01:00
Nick Sieger 965590b378 fix: propagate leading --help to subcommands with a local help flag
- --help before a subcommand is parsed as the root's persistent flag and
  stripped from args, which works for most subcommands since they share
  the same flag object with root
- run/create register their own local help flag (no -h, to avoid a
  shorthand clash with --hostname), which shadows root's and never saw
  the value, so the container ran instead of printing usage
- propagate the parsed value into a subcommand's local help flag when
  one shadows the root's, in HandleGlobalFlags

Fixes #7473

Signed-off-by: Nick Sieger <nick@nicksieger.com>
2026-11-05 09:52:46 -06:00
Drew ErnyandClaude Opus 5.5 191aab9fe8 cli/command/formatter: restore fallback for container names with slashes
Since commit f7a909d56b, ContainerContext.Names returns an empty string
when truncating and every name of the container contains a slash. Before
that change, all names were printed in that case.

Classic Swarm prefixes container names with the node-name
("/<node-name>/<container-name>"), so `docker ps` against a classic Swarm
API printed an empty NAMES column.

Restore the fallback to printing all names when no name without slashes
is found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Drew Erny <derny@mirantis.com>
2026-11-05 08:53:09 -06:00
Sebastiaan van Stijn 682373a887 vendor: github.com/Azure/go-ansiterm 51075c6ea6bc
full diff: https://github.com/Azure/go-ansiterm/compare/8c912ac31dd4...51075c6ea6bc

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 13:44:53 +01:00
Sebastiaan van Stijn 949b412ae3 Merge pull request #7478 from thaJeztah/codecov_oidc_step2
ci: use OIDC authentication for Codecov uploads (remaining workflows)
2026-11-05 11:27:55 +01:00
Sebastiaan van Stijn 57393ea18e ci: use OIDC authentication for Codecov uploads (remaining workflows)
follow-up to 18958e4c02, which updated
the e2e workflow, but forgot to update the other workflows.

Replace the CODECOV_TOKEN secret with GitHub Actions OIDC authentication.

Codecov supports tokenless uploads for public repositories, but
authenticated uploads are still needed to protect coverage results on
protected branches from unauthorized uploads.

Grant `id-token: write` at the job level to allow Codecov to request
a short-lived OIDC token. This permission does not grant repository
write access, and scoping it to the job avoids granting it to other
jobs added to the workflow in the future.

Coverage uploads from forked pull requests continue to use Codecov's
tokenless upload mechanism.

See https://github.com/codecov/codecov-action/blob/v7.1.1/README.md#using-oidc

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 10:54:27 +01:00
Sebastiaan van Stijn 84e55d46d7 Merge pull request #7477 from thaJeztah/codecov_oidc
ci: use OIDC authentication for Codecov uploads
2026-11-05 10:39:45 +01:00
Sebastiaan van Stijn 18958e4c02 ci: use OIDC authentication for Codecov uploads
Replace the CODECOV_TOKEN secret with GitHub Actions OIDC authentication.

Codecov supports tokenless uploads for public repositories, but
authenticated uploads are still needed to protect coverage results on
protected branches from unauthorized uploads.

Grant `id-token: write` at the job level to allow Codecov to request
a short-lived OIDC token. This permission does not grant repository
write access, and scoping it to the job avoids granting it to other
jobs added to the workflow in the future.

Coverage uploads from forked pull requests continue to use Codecov's
tokenless upload mechanism.

See https://github.com/codecov/codecov-action/blob/v7.1.1/README.md#using-oidc

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-05 10:22:53 +01:00
Sebastiaan van Stijn ab54f1f097 Merge pull request #7168 from hirehamir/fix/flaky-test-prune-prompt-termination
fix: flaky `TestPrunePromptTermination` test
2026-11-05 09:04:07 +01:00
Jonathan A. Sternberg d278c1ec68 cli-plugins/plugin: add OnFinalize hook for shutting down otel providers
OTEL providers provided by this library to plugins were not properly
shut down. Metrics from the CLI were forcibly flushed in a separate area
and the `buildx` command does its own flushing, but this didn't apply to
traces.

Add shutdown on post run for plugins using `OnFinalize`. This will allow
compose to use the tracer provider from this library rather than roll
its own custom one.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
2026-11-04 12:45:18 -06:00
Sebastiaan van Stijn fb2fe0eac7 Merge pull request #7468 from thaJeztah/bump_otels
vendor: go.opentelemetry.io/otel v1.47.0, contrib v0.72.0
2026-11-03 22:57:33 +01:00
Paweł Gronowski 920919bd15 Merge pull request #7469 from thaJeztah/bump_compress
vendor: github.com/klauspost/compress v1.20.2 (security)
2026-11-03 22:33:58 +01:00
Paweł Gronowski beafae2696 Merge pull request #7442 from thaJeztah/bump_creds_helpers
vendor: github.com/docker/docker-credential-helpers v0.9.12
2026-11-03 22:30:04 +01:00
Sebastiaan van Stijn b9456fd725 vendor: github.com/klauspost/compress v1.20.2
includes a fix for CVE-2026-116591 / [GHSA-fpmh-frq8-7rj8]

full diff: https://github.com/klauspost/compress/compare/v1.20.1...v1.20.2

[GHSA-fpmh-frq8-7rj8]: https://github.com/klauspost/compress/security/advisories/GHSA-fpmh-frq8-7rj8

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-03 22:27:00 +01:00
Sebastiaan van Stijn 7a75f75b96 vendor: go.opentelemetry.io/otel v1.47.0, contrib v0.72.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-03 22:23:13 +01:00
Paweł Gronowski 9762c1688f Merge pull request #7449 from thaJeztah/manifest_completion
manifest: annotate: add shell-completion for --os and --arch
2026-11-03 21:53:15 +01:00
Sebastiaan van Stijn b2f5d905c7 Merge pull request #7466 from thaJeztah/bump_swarmkit
vendor: github.com/moby/swarmkit/v2 v2.1.3
2026-11-03 21:20:00 +01:00
Sebastiaan van Stijn 77ebef3df8 vendor: github.com/moby/swarmkit/v2 v2.1.3
full diff: https://github.com/moby/swarmkit/compare/v2.1.2...v2.1.3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-03 21:11:01 +01:00
Paweł Gronowski 6d15cedd97 Merge pull request #7465 from thaJeztah/bump_x_net
vendor: golang.org/x/net v0.62.0
2026-11-03 17:57:57 +01:00
Sebastiaan van Stijn 82b833e57a vendor: golang.org/x/net v0.62.0
full diff: https://github.com/golang/net/compare/v0.61.0...v0.62.0

This release addresss the following security issues:

- html: avoid quadratic complexity when coalescing text nodes

    When coalescing adjacent text nodes in addText and fosterParent,
    appending to an existing TextNode's Data via string concatenation
    (n.Data += text) reallocated and copied the accumulated string on every
    token. Inputs containing many short text runs separated by ignored end
    tags (such as "a") caused O(n^2) allocations and byte copies.

    Accumulate coalesced text in per-node byte slices during parsing and
    materialize Data strings once at the end of parse.

    Thanks to Markus Magnuson for reporting this issue.

    This is CVE-2026-78661 and Go issue https://go.dev/issue/82224.

- html: check all ancestor namespace transitions when rendering raw text

    In childTextNodesAreLiteral, walking up the tree previously stopped at
    the nearest foreign ancestor once an HTML integration point was found.
    However, that integration point may itself be a descendant of an element
    that was foster-parented into a foreign namespace (such as or
    fostered under ), causing a parser to resolve the
    subtree in a different foreign namespace upon re-parsing the rendered
    output.

    Continue walking the full ancestor chain and verify that every
    transition from a non-empty namespace occurs at an HTML integration
    point before emitting literal text.

    Thanks to Ali Sherif (https://www.linkedin.com/in/ali-sherif-13812b276) for reporting this issue.

    This is CVE-2026-97045 and Go issue https://go.dev/issue/82622.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-03 14:55:15 +01:00
Paweł Gronowski 997674f459 Merge pull request #7462 from thaJeztah/bump_jose2
vendor: github.com/go-jose/go-jose/v4 v4.1.7 (security)
2026-11-03 14:41:36 +01:00
Sebastiaan van Stijn 81e4a109a3 vendor: github.com/go-jose/go-jose/v4 v4.1.7
Fixed Security Issues

- Thumbprint: reject invalid RSA key [GHSA-r39w-98wm-9pp6]
- json: backport stdlib fix for embedded unexported field [GHSA-27pc-rm86-68pq]
- Detect if headers are set by presence in the map [GHSA-p9j4-2j95-j4rv]
- limit RSA size in certificate verify [GHSA-v6h2-qj3f-2h6v]
- Limit recipients and signatures in JWE/JWS [GHSA-c67m-wr4h-2ccg]

Fixed

- jwk: fix bug in x5c parsing
- reject base64-encoded "null" in protected headers

Deprecated

- deprecate ParseEncrypted / ParseSigned

full diff: https://github.com/go-jose/go-jose/compare/v4.1.6...v4.1.7

[GHSA-r39w-98wm-9pp6]: https://github.com/go-jose/go-jose/security/advisories/GHSA-r39w-98wm-9pp6
[GHSA-27pc-rm86-68pq]: https://github.com/go-jose/go-jose/security/advisories/GHSA-27pc-rm86-68pq
[GHSA-p9j4-2j95-j4rv]: https://github.com/go-jose/go-jose/security/advisories/GHSA-p9j4-2j95-j4rv
[GHSA-v6h2-qj3f-2h6v]: https://github.com/go-jose/go-jose/security/advisories/GHSA-v6h2-qj3f-2h6v
[GHSA-c67m-wr4h-2ccg]: https://github.com/go-jose/go-jose/security/advisories/GHSA-c67m-wr4h-2ccg

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-03 14:24:18 +01:00
Paweł Gronowski e7ece565b2 Merge pull request #7460 from thaJeztah/bump_yaml
vendor: go.yaml.in/yaml/v3 v3.0.6 (security)
2026-11-03 14:20:18 +01:00
Paweł Gronowski 4214721b3b Merge pull request #7443 from thaJeztah/bump_jose
vendor: github.com/go-jose/go-jose/v4 v4.1.6 (security)
2026-11-03 14:20:05 +01:00
Sebastiaan van Stijn 281291299e Merge pull request #7459 from renovate-bot/renovate/docker-compose-bin-5.x
chore(deps): update docker/compose-bin docker tag to v5.6.1
2026-11-03 12:01:01 +01:00
Sebastiaan van Stijn 796f9633b4 vendor: go.yaml.in/yaml/v3 v3.0.6
resolve: hint-class literal merge-key string "<<" so it's quoted on encode

full diff: https://github.com/yaml/go-yaml/compare/v3.0.5...v3.0.6

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-11-03 11:54:24 +01:00
Mend Renovate 29b22240bc chore(deps): update docker/compose-bin docker tag to v5.6.1
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-11-03 10:45:47 +00:00
Sebastiaan van Stijn 19a353f2ea Merge pull request #7452 from renovate-bot/renovate/docker-buildx-bin-0.x
chore(deps): update docker/buildx-bin docker tag to v0.38.1
2026-11-03 11:44:17 +01:00
Mend Renovate 3bc38f9c19 chore(deps): update docker/buildx-bin docker tag to v0.38.1
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-11-02 14:10:44 +00:00
Sebastiaan van Stijn 0ccbdf86a0 Merge pull request #7430 from docker/dependabot/github_actions/codeql-actions-da882c3ad8
build(deps): bump the codeql-actions group across 1 directory with 3 updates
2026-11-02 15:09:43 +01:00
Sebastiaan van Stijn fbafb4965c Merge pull request #7441 from docker/dependabot/github_actions/docker-actions-1541f28a00
build(deps): bump the docker-actions group across 1 directory with 2 updates
2026-11-02 15:08:48 +01:00
dependabot[bot] 276829e744 build(deps): bump the codeql-actions group across 1 directory with 3 updates
Bumps the codeql-actions group with 3 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.38.3 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

Updates `github/codeql-action/autobuild` from 4.38.3 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

Updates `github/codeql-action/analyze` from 4.38.3 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: codeql-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 08:46:14 +00:00
dependabot[bot] cc8c87844d build(deps): bump the docker-actions group across 1 directory with 2 updates
Bumps the docker-actions group with 2 updates in the / directory: [docker/github-builder/.github/workflows/bake.yml](https://github.com/docker/github-builder) and [docker/docker-agent-action/.github/workflows/review-pr.yml](https://github.com/docker/docker-agent-action).


Updates `docker/github-builder/.github/workflows/bake.yml` from 1.18.0 to 1.18.1
- [Release notes](https://github.com/docker/github-builder/releases)
- [Commits](https://github.com/docker/github-builder/compare/4345a1cea1e4cfb65c8a5e2b1b80f9e630068827...cbc9d42f8c89a55623392aadd03600965b80e963)

Updates `docker/docker-agent-action/.github/workflows/review-pr.yml` from 2.0.9 to 2.1.0
- [Release notes](https://github.com/docker/docker-agent-action/releases)
- [Commits](https://github.com/docker/docker-agent-action/compare/3654ec391b070e247de2e2e3c1ff2519b6378b3a...aaa657c53cc69ed15683cd8a985d86663a0e1594)

---
updated-dependencies:
- dependency-name: docker/docker-agent-action/.github/workflows/review-pr.yml
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-actions
- dependency-name: docker/github-builder/.github/workflows/bake.yml
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 08:43:09 +00:00
Sebastiaan van Stijn e512522843 manifest: annotate: add shell-completion for --os and --arch
With this patch;

    docker manifest annotate --os <TAB>
    aix        darwin     freebsd    illumos    js         nacl       openbsd    solaris    zos
    android    dragonfly  hurd       ios        linux      netbsd     plan9      windows

    docker manifest annotate --arch <TAB>
    386          arm          armbe        mips64       mips64p32le  ppc64        riscv64      sparc
    amd64        arm64        loong64      mips64le     mipsle       ppc64le      s390         sparc64
    amd64p32     arm64be      mips         mips64p32    ppc          riscv        s390x        wasm

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-30 15:03:10 +01:00
Sebastiaan van Stijn 8c8a77521a Merge pull request #7448 from thaJeztah/carry_7447
manifest: add loong64 to manifest annotate, and remove hard-coded os/arch list in validation
2026-10-30 14:59:17 +01:00
Sebastiaan van Stijn bf8066ca18 manifest: remove hard-coded os/arch list in validation
Replace the hardcoded list of supported OS/architecture combinations with
syntax validation using containerd/platforms.

This allows annotating manifests with platforms that are not explicitly
known to the CLI, while still rejecting missing or malformed values.

The validation uses a FormatAll/Parse roundtrip until containerd/platforms
provides a dedicated validation function.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-30 13:12:26 +01:00
吴小白 d35139c89f manifest: add loong64 to manifest annotate bash completion
Signed-off-by: 吴小白 <296015668@qq.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-30 11:25:59 +01:00
Sebastiaan van Stijn b32aa42608 Merge pull request #7445 from thaJeztah/geen_kabouters
scripts/build: omit DWARF debug information by default
2026-10-29 21:33:03 +01:00
Sebastiaan van Stijn 8ff6f243a2 scripts/build: omit DWARF debug information by default
Pass -w to the Go linker to reduce binary size while retaining the
symbol table for tools that inspect Go binaries.

For a linux/arm64 build, this reduces the CLI binary from 39,809,791
to 31,310,197 bytes: a saving of 8.5 MB (21.4%).

Place the default before caller-supplied GO_LDFLAGS so builds can
retain DWARF by passing GO_LDFLAGS="-w=0".

Before

    ls -l /go/src/github.com/docker/cli/build/docker-linux-arm64
    -rwxr-xr-x 1 root root 39809791 Oct 29 17:34 /go/src/github.com/docker/cli/build/docker-linux-arm64

After

    ls -l /go/src/github.com/docker/cli/build/docker-linux-arm64
    -rwxr-xr-x 1 root root 31310197 Oct 29 17:36 /go/src/github.com/docker/cli/build/docker-linux-arm64

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-29 19:06:26 +01:00
Sebastiaan van Stijn b9312b9d88 scripts/build: suppress shellcheck linting
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-29 18:29:37 +01:00
Sebastiaan van Stijn ccccec73b0 vendor: github.com/go-jose/go-jose/v4 v4.1.6
- contains a fix for CVE-2026-116587 / [GHSA-mgcc-cww5-fp2q]

full diff: https://github.com/go-jose/go-jose/compare/v4.1.5...v4.1.6

[GHSA-mgcc-cww5-fp2q]: https://github.com/go-jose/go-jose/security/advisories/GHSA-mgcc-cww5-fp2q

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-28 22:38:48 +01:00
Sebastiaan van Stijn dd8edb6863 vendor: github.com/docker/docker-credential-helpers v0.9.12
no changes in vendored code

full diff: https://github.com/docker/docker-credential-helpers/compare/v0.9.9...v0.9.12

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-28 14:23:04 +01:00
Sebastiaan van Stijn 66786b775a Merge pull request #7421 from thaJeztah/bump_x_deps
vendor: update golang.org/x/* dependencies
2026-10-27 10:20:11 +01:00
Paweł Gronowski 47e9dbf7b0 Merge pull request #7427 from renovate-bot/renovate/docker-actions
Update docker GitHub Actions
2026-10-26 20:12:30 +01:00
Mend Renovate abf2f34c74 Update docker GitHub Actions
Signed-off-by: Mend Renovate <bot@renovateapp.com>
2026-10-26 12:59:35 +00:00
Hamir dd50814004 fix: flaky TestPrunePromptTermination test
TerminatePrompt created a pipe for the prompt's stdin but discarded the
write end. Once the garbage collector finalized it, the write end was
closed, the prompt read EOF and treated it as "no", and the command
returned "prune has been cancelled" instead of prompt.ErrTerminated.

This keeps both ends of the pipe referenced and closes them in t.Cleanup.

This change also drops the OutBuffer().Len() check:
the writer hook only fires after
the write has completed, so the check proved nothing, and it raced with
the prompt's later writes under -race.

This change also applies the same fix to the identical copy in cmd/docker-trust.

Signed-off-by: Hamir <hirehamir@gmail.com>
2026-10-22 10:04:59 -07:00
Sebastiaan van Stijn 6998c79cfd vendor: golang.org/x/net v0.61.0
- quic: properly check for allowed frame types in a packet

full diff: https://github.com/golang/net/compare/v0.60.0...v0.61.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-20 13:41:27 +02:00
Sebastiaan van Stijn c7bdacad70 vendor: golang.org/x/text v0.43.0
- secure/bidirule: lazily precompute ASCII table
- unicode/bidi: avoid quadratic behavior in Paragraph.Order and Paragraph.Line

full diff: https://github.com/golang/text/compare/v0.42.0...v0.43.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-20 13:40:24 +02:00
Sebastiaan van Stijn 1faf23e25f vendor: golang.org/x/term v0.47.0
full diff: https://github.com/golang/term/compare/v0.46.0...v0.47.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-20 13:39:46 +02:00
Sebastiaan van Stijn b8dd18fe2c vendor: golang.org/x/sync v0.24.0
full diff: https://github.com/golang/sync/compare/v0.23.0...v0.24.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-20 13:39:09 +02:00
Sebastiaan van Stijn a3539bd5c9 vendor: golang.org/x/mod v0.42.0
full diff: https://github.com/golang/mod/compare/v0.41.0...v0.42.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-20 13:38:32 +02:00
Sebastiaan van Stijn adfb114576 vendor: golang.org/x/sys v0.49.0
- cpu: add amd64 v2/v3/v4 feature bits
- darwin: expose setpriority PRIO_DARWIN_* constants
- unix/darwin: add missing pwritev test case on darwin/arm64
- unix/darwin: add O_EXEC and O_SEARCH on darwin
- unix/darwin: pass fcntl data as unsafe.Pointer in FcntlF{lock,store} on darwin
- unix/linux: don't warn about closed stderr from child process
- unix/linux: unify ppc64x syscall wrappers
- unix: add listns syscall introduced in Linux 6.19
- unix: add Mmsghdr for linux
- unix: add PrctlArg5Ptr
- unix: align the getdents buffer
- unix: extend z/OS support
- unix: keep generated types compilable with current cgo
- unix: pass xattr data as unsafe.Pointer in BSD xattr syscalls
- unix: provide linux/sparc64 assembly for syscalls
- unix: return syscall error in *listxattrNS on BSD
- unix: update glibc to 2.44
- unix: update linux to v7.2
- unix: use the stat64 family and fix Stat_t on linux/sparc64
- windows: drop arm

full diff: https://github.com/golang/sys/compare/v0.48.0...v0.49.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-20 13:37:40 +02:00
342 changed files with 8896 additions and 18590 deletions
+3 -3
View File
@@ -68,7 +68,7 @@ jobs:
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
-
name: Build
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
with:
targets: ${{ matrix.target }}
set: |
@@ -99,7 +99,7 @@ jobs:
bin-image:
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
uses: docker/github-builder/.github/workflows/bake.yml@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a # v1.17.0
uses: docker/github-builder/.github/workflows/bake.yml@cbc9d42f8c89a55623392aadd03600965b80e963 # v1.18.1
permissions:
contents: read # same as global permission
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
@@ -159,7 +159,7 @@ jobs:
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
-
name: Build
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
with:
targets: plugins-cross
set: |
+4 -4
View File
@@ -64,18 +64,18 @@ jobs:
name: Update Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.10"
go-version: "1.27.3"
cache: false
-
name: Initialize CodeQL
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
category: "/language:go"
+4 -1
View File
@@ -26,6 +26,9 @@ on:
jobs:
tests:
permissions:
contents: read
id-token: write # needed for CodeCov; https://github.com/codecov/codecov-action/blob/v7.1.1/README.md#using-oidc
runs-on: ubuntu-26.04
strategy:
fail-fast: false
@@ -80,4 +83,4 @@ jobs:
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
files: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
use_oidc: true
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
jobs:
review:
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@3654ec391b070e247de2e2e3c1ff2519b6378b3a # v2.0.9
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@aaa657c53cc69ed15683cd8a985d86663a0e1594 # v2.1.0
permissions:
contents: read # Read repository files and PR diffs
pull-requests: write # Post review comments
+12 -6
View File
@@ -26,6 +26,9 @@ on:
jobs:
ctn:
permissions:
contents: read
id-token: write # needed for CodeCov; https://github.com/codecov/codecov-action/blob/v7.1.1/README.md#using-oidc
runs-on: ubuntu-26.04
steps:
-
@@ -33,7 +36,7 @@ jobs:
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
-
name: Test
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
with:
targets: test-coverage
-
@@ -41,9 +44,12 @@ jobs:
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
files: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
use_oidc: true
host:
permissions:
contents: read
id-token: write # needed for CodeCov; https://github.com/codecov/codecov-action/blob/v7.1.1/README.md#using-oidc
runs-on: ${{ matrix.os }}
env:
GOPATH: ${{ github.workspace }}
@@ -53,9 +59,9 @@ jobs:
fail-fast: false
matrix:
os:
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
- macos-15-intel # macOS 15 on Intel
- macos-15 # macOS 15 on arm64 (Apple Silicon M1)
- macos-26 # macOS 26 on arm64 (Apple Silicon M1)
- macos-26-intel # macOS 26 on Intel
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
steps:
-
@@ -68,7 +74,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.10"
go-version: "1.27.3"
cache: false
-
name: Test
@@ -86,4 +92,4 @@ jobs:
with:
files: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
token: ${{ secrets.CODECOV_TOKEN }}
use_oidc: true
+2 -2
View File
@@ -38,7 +38,7 @@ jobs:
steps:
-
name: Run
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
uses: docker/bake-action@59cb1203cd42bcefd40a1b6d73b589e48c86040e # v7.5.0
with:
targets: ${{ matrix.target }}
@@ -101,7 +101,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.10"
go-version: "1.27.3"
cache: false
-
name: Run gocompat check
+1 -11
View File
@@ -5,7 +5,7 @@ run:
# which causes it to fallback to go1.17 semantics.
#
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.26.10"
go: "1.27.3"
timeout: 5m
@@ -201,11 +201,6 @@ linters:
linters:
- gosec
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
- text: "package-comments: should have a package comment"
linters:
- revive
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
@@ -227,11 +222,6 @@ linters:
linters:
- staticcheck
# TODO(thaJeztah): remove once https://github.com/leighmcculloch/gocheckcompilerdirectives/issues/7 is fixed.
- text: "compiler directive unrecognized: //go:fix"
linters:
- gocheckcompilerdirectives
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
+8 -6
View File
@@ -5,10 +5,12 @@ ARG BASE_VARIANT=alpine
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.23
ARG BASE_DEBIAN_DISTRO=bookworm
ARG ALPINE_VERSION=3.24
ARG GO_VERSION=1.26.10
# BASE_DEBIAN_DISTRO sets the version of the debian base image to use,
ARG BASE_DEBIAN_DISTRO=trixie
ARG GO_VERSION=1.27.3
# XX_VERSION specifies the version of the xx utility to use.
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
@@ -25,12 +27,12 @@ ARG GOTESTSUM_VERSION=v1.13.0
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
# It must be a tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub.
ARG BUILDX_VERSION=0.38.0
ARG BUILDX_VERSION=0.38.1
# COMPOSE_VERSION is the version of compose to install in the dev container.
# It must be a tag in the docker.io/docker/compose-bin image repository
# on Docker Hub.
ARG COMPOSE_VERSION=v5.6.0
ARG COMPOSE_VERSION=v5.7.0
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
@@ -53,7 +55,7 @@ WORKDIR /go/src/github.com/docker/cli
FROM build-base-debian AS build-debian
ARG TARGETPLATFORM
RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-12-dev pkgconf
RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-14-dev pkgconf
FROM build-base-${BASE_VARIANT} AS goversioninfo
ARG GOVERSIONINFO_VERSION
+62 -2
View File
@@ -10,6 +10,7 @@ import (
"fmt"
"os"
"sync"
"time"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata"
@@ -32,6 +33,11 @@ import (
// called.
var PersistentPreRunE func(*cobra.Command, []string) error
// finalizeHookTimeout is the timeout given for running the finalize hook.
// It contains the timeout for exporting metrics and a little bit extra in case
// shutdown does not respond to the export timeout promptly.
const finalizeHookTimeout = command.ExportTimeout + 10*time.Millisecond
// RunPlugin executes the specified plugin command
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error {
tcmd := newPluginCommand(dockerCli, plugin, meta)
@@ -69,6 +75,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
stopInstrumentation(err)
return err
}
cobra.OnFinalize(func() { finalizeHook(dockerCli) })
})
return retErr
}
@@ -83,6 +90,59 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
return cmd.Execute()
}
// finalizeHook is a hook to be run when the CLI has finished executing the run command.
// This happens after any post run methods but also always runs even when an error happens.
func finalizeHook(dockerCli *command.DockerCli) {
// Use a new context for the export so that the command being cancelled
// doesn't affect the metrics, and we get metrics for cancelled commands.
ctx, cancel := context.WithTimeout(context.Background(), command.ExportTimeout)
defer cancel()
var (
wg sync.WaitGroup
wait bool
)
if tp, ok := dockerCli.TracerProvider().(command.TracerProvider); ok {
wg.Go(func() {
if err := tp.Shutdown(ctx); err != nil {
otel.Handle(err)
}
})
wait = true
}
if mp, ok := dockerCli.MeterProvider().(command.MeterProvider); ok {
wg.Go(func() {
if err := mp.Shutdown(ctx); err != nil {
otel.Handle(err)
}
})
wait = true
}
// Wait for the wait group in another goroutine if any were created.
if !wait {
return
}
// Spawn a goroutine to wait for the wait group.
// If the shutdown does not promptly hang up with the
// cancellation of the context, we leave this goroutine
// orphaned so the process can exit.
done := make(chan struct{})
go func() {
defer close(done)
wg.Wait()
}()
select {
case <-done:
case <-time.After(finalizeHookTimeout):
// Ensure we still exit even if the shutdown doesn't work properly.
// We give 10 extra milliseconds for the cleanup to happen.
}
}
// Run is the top-level entry point to the CLI plugin framework. It should
// be called from the plugin's "main()" function. It initializes a new
// [command.DockerCli] instance with the given options before calling
@@ -212,8 +272,8 @@ func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra
Use: metadata.MetadataSubcommandName,
Hidden: true,
// Suppress the global/parent PersistentPreRunE, which
// needlessly initializes the client and tries to
// connect to the daemon.
// needlessly initializes the client and tries to connect to the
// daemon.
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
RunE: func(cmd *cobra.Command, args []string) error {
enc := json.NewEncoder(os.Stdout)
+19 -1
View File
@@ -160,7 +160,25 @@ func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, erro
return nil, nil, cmd.FlagErrorFunc()(cmd, err)
}
return cmd, flags.Args(), nil
args := flags.Args()
// A leading "--help"/"-h" is consumed above as the root's persistent
// help flag and stripped from args, so the target subcommand never sees
// it. That's fine for most subcommands, which inherit the same help
// flag instance, but "run" and "create" register their own local
// "help" flag (to avoid a shorthand clash with "-h"/"--hostname"),
// which shadows the persistent one and is left false. Propagate the
// value so "docker --help run <image>" shows help like "docker run
// --help" does, instead of running the container.
if help, _ := flags.GetBool("help"); help {
if sub, _, err := cmd.Find(args); err == nil && sub != cmd {
if sub.Flags().Lookup("help") != nil {
_ = sub.Flags().Set("help", "true")
}
}
}
return cmd, args, nil
}
// Initialize finalises global option parsing and initializes the docker client.
+42
View File
@@ -108,3 +108,45 @@ func TestDecoratedName(t *testing.T) {
topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"}
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*")
}
// TestHandleGlobalFlagsHelpPropagation covers https://github.com/docker/cli/issues/7473:
// a leading "--help" is consumed as the root's persistent help flag and
// stripped from args, so a subcommand that registers its own local "help"
// flag (as "run" and "create" do, to avoid a shorthand clash with "-h") never
// sees it and runs instead of showing help.
func TestHandleGlobalFlagsHelpPropagation(t *testing.T) {
newRootCmd := func() (root, sub *cobra.Command) {
root = &cobra.Command{Use: "docker"}
root.PersistentFlags().BoolP("help", "h", false, "Print usage")
sub = &cobra.Command{Use: "run", RunE: func(cmd *cobra.Command, args []string) error { return nil }}
sub.Flags().Bool("help", false, "Print usage")
root.AddCommand(sub)
return root, sub
}
t.Run("propagates to the shadowing local help flag", func(t *testing.T) {
root, sub := newRootCmd()
tcmd := &TopLevelCommand{cmd: root, args: []string{"--help", "run", "hello-world"}}
_, args, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
assert.DeepEqual(t, args, []string{"run", "hello-world"})
help, err := sub.Flags().GetBool("help")
assert.NilError(t, err)
assert.Equal(t, help, true)
})
t.Run("leaves the local help flag alone without --help", func(t *testing.T) {
root, sub := newRootCmd()
tcmd := &TopLevelCommand{cmd: root, args: []string{"run", "hello-world"}}
_, _, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
help, err := sub.Flags().GetBool("help")
assert.NilError(t, err)
assert.Equal(t, help, false)
})
}
-19
View File
@@ -16,7 +16,6 @@ import (
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
configtypes "github.com/docker/cli/cli/config/types"
dcontext "github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
@@ -24,7 +23,6 @@ import (
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/version"
"github.com/docker/cli/internal/secretsengine"
dopts "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/build"
"github.com/moby/moby/client"
@@ -77,8 +75,6 @@ type DockerCli struct {
contextStoreConfig *store.Config
initTimeout time.Duration
res telemetryResource
registryAuthOnce sync.Once
registryAuth secretsengine.AuthProvider
// baseCtx is the base context used for internal operations. In the future
// this may be replaced by explicitly passing a context to functions that
@@ -145,21 +141,6 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
return cli.configFile
}
func (cli *DockerCli) RegistryAuthProvider() secretsengine.AuthProvider {
cli.registryAuthOnce.Do(func() {
cli.registryAuth = secretsengine.NewDesktop(configFileAuth{cli})
})
return cli.registryAuth
}
type configFileAuth struct {
cli *DockerCli
}
func (c configFileAuth) GetAuthConfig(registryHostname string) (configtypes.AuthConfig, error) {
return c.cli.ConfigFile().GetAuthConfig(registryHostname)
}
// ServerInfo returns the server version details for the host this client is
// connected to
func (cli *DockerCli) ServerInfo() ServerInfo {
-15
View File
@@ -18,7 +18,6 @@ import (
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/flags"
"github.com/moby/moby/client"
@@ -502,17 +501,3 @@ func TestNewDockerCliWithAPIClientOptions(t *testing.T) {
assert.NilError(t, err)
assert.DeepEqual(t, received, "fake-agent/0.0.1")
}
func TestRegistryAuthProvider(t *testing.T) {
t.Parallel()
cli := &DockerCli{configFile: &configfile.ConfigFile{}}
p := cli.RegistryAuthProvider()
assert.Assert(t, p == cli.RegistryAuthProvider(), "the provider must be created once per DockerCli")
cli.configFile = &configfile.ConfigFile{AuthConfigs: map[string]configtypes.AuthConfig{
"ghcr.io": {Username: "user", Password: "pass"},
}}
got, err := p.GetAuthConfig("ghcr.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, configtypes.AuthConfig{Username: "user", Password: "pass"})
}
+1 -2
View File
@@ -21,7 +21,6 @@ import (
"github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/internal/registryauth"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/client"
@@ -134,7 +133,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
}
func pullImage(ctx context.Context, dockerCLI command.Cli, img string, options *createOptions) error {
encodedAuth, err := registryauth.EncodedAuth(dockerCLI, img)
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), img)
if err != nil {
return err
}
+15 -12
View File
@@ -146,24 +146,27 @@ func (c *ContainerContext) ID() string {
// slash (/) prefix stripped. Additional names for the container (related to the
// legacy `--link` feature) are omitted when formatting "truncated".
func (c *ContainerContext) Names() string {
var b strings.Builder
for i, n := range c.c.Names {
name := strings.TrimPrefix(n, "/")
if c.trunc {
// When printing truncated, we only print a single name.
//
// Pick the first name that's not a legacy link (does not have
// slashes inside the name itself (e.g., "/other-container/link")).
// Normally this would be the first name found.
if strings.IndexByte(name, '/') == -1 {
if c.trunc {
// When printing truncated, we only print a single name.
//
// Pick the first name that's not a legacy link (does not have
// slashes inside the name itself (e.g., "/other-container/link")).
// Normally this would be the first name found.
for _, n := range c.c.Names {
if name := strings.TrimPrefix(n, "/"); strings.IndexByte(name, '/') == -1 {
return name
}
continue
}
// No name without slashes was found; this can happen when connected
// to a classic Swarm API, which prefixes names with the node-name
// ("/<node-name>/<container-name>"). Fall back to printing all names.
}
var b strings.Builder
for i, n := range c.c.Names {
if i > 0 {
b.WriteByte(',')
}
b.WriteString(name)
b.WriteString(strings.TrimPrefix(n, "/"))
}
return b.String()
}
+20
View File
@@ -48,6 +48,25 @@ func TestContainerPsContext(t *testing.T) {
expValue: "foobar_baz",
call: ctx.Names,
},
{
container: container.Summary{Names: []string{"/other/link", "/foobar_baz"}},
trunc: true,
expValue: "foobar_baz",
call: ctx.Names,
},
{
container: container.Summary{Names: []string{"/foobar_baz", "/other/link"}},
expValue: "foobar_baz,other/link",
call: ctx.Names,
},
{
// Names returned by a classic Swarm API are prefixed with the
// node-name; see https://github.com/docker/cli/pull/6875
container: container.Summary{Names: []string{"/node-1/foobar_baz"}},
trunc: true,
expValue: "node-1/foobar_baz",
call: ctx.Names,
},
{
container: container.Summary{Image: "ubuntu"},
trunc: true,
@@ -257,6 +276,7 @@ func TestContainerPsContext(t *testing.T) {
}
}
//nolint:dupword // ignore duplicate "ubuntu" in test fixtures
func TestContainerContextWrite(t *testing.T) {
unixTime := time.Now().AddDate(0, 0, -1).Unix()
expectedTime := time.Unix(unixTime, 0).String()
+1 -2
View File
@@ -14,7 +14,6 @@ import (
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/internal/registryauth"
"github.com/moby/moby/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
@@ -91,7 +90,7 @@ func runPull(ctx context.Context, dockerCLI command.Cli, opts pullOptions) error
ociPlatforms = append(ociPlatforms, p)
}
encodedAuth, err := registryauth.EncodedAuth(dockerCLI, distributionRef.String())
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), distributionRef.String())
if err != nil {
return err
}
+1 -2
View File
@@ -17,7 +17,6 @@ import (
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/internal/registryauth"
"github.com/docker/cli/internal/tui"
"github.com/moby/moby/api/types/auxprogress"
"github.com/moby/moby/client"
@@ -109,7 +108,7 @@ To push the complete multi-platform image, remove the --platform flag.
}
// Resolve the Auth config relevant for this server
encodedAuth, err := registryauth.EncodedAuth(dockerCli, ref.String())
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), ref.String())
if err != nil {
return err
}
+30 -2
View File
@@ -10,8 +10,10 @@ import (
"slices"
"github.com/containerd/errdefs"
"github.com/containerd/platforms"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/manifest/store"
"github.com/docker/cli/internal/registryclient"
@@ -20,6 +22,23 @@ import (
"github.com/spf13/cobra"
)
var (
// similar to https://github.com/containerd/platforms/blob/v1.0.0-rc.5/database.go#L29-L38
knownOSes = []string{
"aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos",
"ios", "js", "linux", "nacl", "netbsd", "openbsd", "plan9", "solaris",
"windows", "zos",
}
// similar to https://github.com/containerd/platforms/blob/v1.0.0-rc.5/database.go#L51-L60
knownArches = []string{
"386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64",
"ppc64le", "loong64", "mips", "mipsle", "mips64", "mips64le", "mips64p32",
"mips64p32le", "ppc", "riscv", "riscv64", "s390", "s390x", "sparc",
"sparc64", "wasm",
}
)
type annotateOptions struct {
target string // the target manifest list name (also transaction ID)
image string // the manifest to annotate within the list
@@ -97,6 +116,8 @@ func newAnnotateCommand(dockerCLI command.Cli) *cobra.Command {
flags.StringSliceVar(&opts.osFeatures, "os-features", []string{}, "Set operating system feature")
flags.StringVar(&opts.variant, "variant", "", "Set architecture variant")
_ = cmd.RegisterFlagCompletionFunc("os", completion.FromList(knownOSes...))
_ = cmd.RegisterFlagCompletionFunc("arch", completion.FromList(knownArches...))
return cmd
}
@@ -139,8 +160,15 @@ func runManifestAnnotate(dockerCLI command.Cli, opts annotateOptions) error {
imageManifest.Descriptor.Platform.OSVersion = opts.osVersion
}
if !isValidOSArch(imageManifest.Descriptor.Platform.OS, imageManifest.Descriptor.Platform.Architecture) {
return fmt.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
// Validate platform syntax, without restricting OS/architecture combinations.
//
// TODO(thaJeztah): Implement a platforms.Validate in containerd/platforms and replace this code.
p := imageManifest.Descriptor.Platform
if p.OS == "" || p.Architecture == "" {
return fmt.Errorf("manifest entry for image has missing os/architecture: %q/%q", p.OS, p.Architecture)
}
if _, err := platforms.Parse(platforms.FormatAll(*p)); err != nil {
return fmt.Errorf("manifest entry for image has invalid platform: %w", err)
}
return manifestStore.Save(targetRef, imgRef, imageManifest)
}
+8 -7
View File
@@ -58,15 +58,16 @@ func TestManifestAnnotate(t *testing.T) {
assert.ErrorContains(t, cmd.Execute(), expectedError)
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
cmd.Flags().Set("os", "freebsd")
cmd.Flags().Set("arch", "fake")
cmd.Flags().Set("os-version", "1")
cmd.Flags().Set("os-features", "feature1")
cmd.Flags().Set("variant", "v7")
expectedError = "manifest entry for image has unsupported os/arch combination"
assert.NilError(t, cmd.Flags().Set("os", "freebsd"))
assert.NilError(t, cmd.Flags().Set("arch", "<invalid>"))
assert.NilError(t, cmd.Flags().Set("os-version", "1"))
assert.NilError(t, cmd.Flags().Set("os-features", "feature1"))
assert.NilError(t, cmd.Flags().Set("variant", "v7"))
expectedError = "manifest entry for image has invalid platform"
assert.ErrorContains(t, cmd.Execute(), expectedError)
cmd.Flags().Set("arch", "arm")
assert.NilError(t, cmd.Flags().Set("arch", "arm"))
assert.NilError(t, cmd.Execute())
cmd = newInspectCommand(cli)
-47
View File
@@ -9,53 +9,6 @@ import (
"github.com/docker/cli/cli/manifest/types"
)
type osArch struct {
os string
arch string
}
// Remove any unsupported os/arch combo
// list of valid os/arch values (see "Optional Environment Variables" section
// of https://go.dev/doc/install/source
// Added linux/s390x as we know System z support already exists
// Keep in sync with _docker_manifest_annotate in contrib/completion/bash/docker
var validOSArches = map[osArch]bool{
{os: "darwin", arch: "386"}: true,
{os: "darwin", arch: "amd64"}: true,
{os: "darwin", arch: "arm"}: true,
{os: "darwin", arch: "arm64"}: true,
{os: "dragonfly", arch: "amd64"}: true,
{os: "freebsd", arch: "386"}: true,
{os: "freebsd", arch: "amd64"}: true,
{os: "freebsd", arch: "arm"}: true,
{os: "linux", arch: "386"}: true,
{os: "linux", arch: "amd64"}: true,
{os: "linux", arch: "arm"}: true,
{os: "linux", arch: "arm64"}: true,
{os: "linux", arch: "ppc64le"}: true,
{os: "linux", arch: "mips64"}: true,
{os: "linux", arch: "mips64le"}: true,
{os: "linux", arch: "riscv64"}: true,
{os: "linux", arch: "s390x"}: true,
{os: "netbsd", arch: "386"}: true,
{os: "netbsd", arch: "amd64"}: true,
{os: "netbsd", arch: "arm"}: true,
{os: "openbsd", arch: "386"}: true,
{os: "openbsd", arch: "amd64"}: true,
{os: "openbsd", arch: "arm"}: true,
{os: "plan9", arch: "386"}: true,
{os: "plan9", arch: "amd64"}: true,
{os: "solaris", arch: "amd64"}: true,
{os: "windows", arch: "386"}: true,
{os: "windows", arch: "amd64"}: true,
}
func isValidOSArch(os string, arch string) bool {
// check for existence of this combo
_, ok := validOSArches[osArch{os, arch}]
return ok
}
func normalizeReference(ref string) (reference.Named, error) {
namedRef, err := reference.ParseNormalizedNamed(ref)
if err != nil {
+1 -2
View File
@@ -9,7 +9,6 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/internal/registryauth"
"github.com/moby/moby/api/types/plugin"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
@@ -61,7 +60,7 @@ func buildPullConfig(dockerCLI command.Cli, opts pluginOptions) (client.PluginIn
return client.PluginInstallOptions{}, err
}
encodedAuth, err := registryauth.EncodedAuth(dockerCLI, ref.String())
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), ref.String())
if err != nil {
return client.PluginInstallOptions{}, err
}
+1 -2
View File
@@ -8,7 +8,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/internal/registryauth"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@@ -43,7 +42,7 @@ func runPush(ctx context.Context, dockerCli command.Cli, name string) error {
}
named = reference.TagNameOnly(named)
encodedAuth, err := registryauth.EncodedAuth(dockerCli, named.String())
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), named.String())
if err != nil {
return err
}
+1 -2
View File
@@ -7,7 +7,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/internal/registryauth"
cliopts "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
@@ -121,7 +120,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
if opts.registryAuth {
// Retrieve encoded auth token from the image reference
var err error
encodedAuth, err = registryauth.EncodedAuth(dockerCLI, opts.image)
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), opts.image)
if err != nil {
return err
}
+2 -2
View File
@@ -210,9 +210,9 @@ func TestResourceOptionsToResourceRequirementsSwap(t *testing.T) {
r, err = swapOptions.ToResourceRequirements(flags)
assert.NilError(t, err)
assert.Check(t, r.SwapBytes != nil)
assert.Check(t, is.Equal(*(r.SwapBytes), int64(86000)))
assert.Check(t, is.Equal(*r.SwapBytes, int64(86000)))
assert.Check(t, r.MemorySwappiness != nil)
assert.Check(t, is.Equal(*(r.MemorySwappiness), int64(23)))
assert.Check(t, is.Equal(*r.MemorySwappiness, int64(23)))
}
func TestToServiceNetwork(t *testing.T) {
+1 -2
View File
@@ -17,7 +17,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/internal/registryauth"
"github.com/docker/cli/opts"
"github.com/docker/cli/opts/swarmopts"
"github.com/moby/moby/api/types/container"
@@ -224,7 +223,7 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
// Retrieve encoded auth token from the image reference
// This would be the old image if it didn't change in this update
var err error
encodedAuth, err = registryauth.EncodedAuth(dockerCLI, spec.TaskTemplate.ContainerSpec.Image)
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), spec.TaskTemplate.ContainerSpec.Image)
if err != nil {
return err
}
+1 -2
View File
@@ -10,7 +10,6 @@ import (
"github.com/docker/cli/cli/command/service"
"github.com/docker/cli/cli/compose/convert"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/internal/registryauth"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
@@ -225,7 +224,7 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
if sendAuth {
// Retrieve encoded auth token from the image reference
encodedAuth, err = registryauth.EncodedAuth(dockerCLI, image)
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), image)
if err != nil {
return nil, err
}
+2 -2
View File
@@ -20,7 +20,7 @@ import (
"go.opentelemetry.io/otel/trace"
)
const exportTimeout = 50 * time.Millisecond
const ExportTimeout = 50 * time.Millisecond
// TracerProvider is an extension of the trace.TracerProvider interface for CLI programs.
type TracerProvider interface {
@@ -205,7 +205,7 @@ func (r *cliReader) Shutdown(ctx context.Context) error {
// We don't want CLI metrics to prevent the CLI from exiting
// so if there's some kind of issue we need to abort pretty
// quickly.
ctx, cancel := context.WithTimeout(ctx, exportTimeout)
ctx, cancel := context.WithTimeout(ctx, ExportTimeout)
defer cancel()
return r.ForceFlush(ctx)
+1 -1
View File
@@ -109,7 +109,7 @@ func dockerSpanExporter(ctx context.Context, cli Cli) []sdktrace.TracerProviderO
otel.Handle(err)
return nil
}
return []sdktrace.TracerProviderOption{sdktrace.WithBatcher(exp, sdktrace.WithExportTimeout(exportTimeout))}
return []sdktrace.TracerProviderOption{sdktrace.WithBatcher(exp, sdktrace.WithExportTimeout(ExportTimeout))}
}
func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option {
+1 -1
View File
@@ -85,7 +85,7 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue)
return func(err error) {
// Use a new context for the export so that the command being cancelled
// doesn't affect the metrics, and we get metrics for cancelled commands.
ctx, cancel := context.WithTimeout(context.Background(), exportTimeout)
ctx, cancel := context.WithTimeout(context.Background(), ExportTimeout)
defer cancel()
duration := float64(time.Since(start)) / float64(time.Millisecond)
-4
View File
@@ -255,7 +255,6 @@ func mergeLoggingConfig(dst, src reflect.Value) error {
return nil
}
//nolint:unparam
func mergeUlimitsConfig(dst, src reflect.Value) error {
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
dst.Elem().Set(src.Elem())
@@ -263,7 +262,6 @@ func mergeUlimitsConfig(dst, src reflect.Value) error {
return nil
}
//nolint:unparam
func mergeShellCommand(dst, src reflect.Value) error {
if src.Len() != 0 {
dst.Set(src)
@@ -271,7 +269,6 @@ func mergeShellCommand(dst, src reflect.Value) error {
return nil
}
//nolint:unparam
func mergeServiceNetworkConfig(dst, src reflect.Value) error {
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
dst.Elem().FieldByName("Aliases").Set(src.Elem().FieldByName("Aliases"))
@@ -285,7 +282,6 @@ func mergeServiceNetworkConfig(dst, src reflect.Value) error {
return nil
}
//nolint:unparam
func mergeUint64(dst, src reflect.Value) error {
if !src.IsNil() {
dst.Elem().Set(src.Elem())
+6 -3
View File
@@ -27,8 +27,13 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
}))
cli.SetOut(outStream)
r, _, err := os.Pipe()
r, w, err := os.Pipe()
assert.NilError(t, err)
t.Cleanup(func() {
_ = w.Close()
_ = r.Close()
})
cli.SetIn(streams.NewIn(r))
notifyCtx, notifyCancel := context.WithCancel(ctx)
@@ -58,8 +63,6 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
}()
}
assert.Check(t, cli.OutBuffer().Len() > 0)
// a small delay to ensure the plugin is prompting
time.Sleep(100 * time.Microsecond)
+1
View File
@@ -4156,6 +4156,7 @@ _docker_manifest_annotate() {
amd64
arm
arm64
loong64
mips64
mips64le
ppc64le
+1 -1
View File
@@ -2,7 +2,7 @@
# ALPINE_VERSION sets the version of the alpine base image to use.
# It must be a supported tag in the docker.io/library/alpine image repository.
ARG ALPINE_VERSION=3.23
ARG ALPINE_VERSION=3.24
FROM alpine:${ALPINE_VERSION} AS gen
RUN apk add --no-cache bash git
+3 -3
View File
@@ -1,16 +1,16 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.10
ARG GO_VERSION=1.27.3
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.23
ARG ALPINE_VERSION=3.24
# BUILDX_VERSION sets the version of buildx to install in the dev container.
# It must be a valid tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub.
ARG BUILDX_VERSION=0.38.0
ARG BUILDX_VERSION=0.38.1
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
+3 -3
View File
@@ -1,13 +1,13 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.10
ARG GO_VERSION=1.27.3
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.23
ARG ALPINE_VERSION=3.24
# GOLANGCI_LINT_VERSION sets the version of the golangci/golangci-lint image to use.
ARG GOLANGCI_LINT_VERSION=v2.10.1
ARG GOLANGCI_LINT_VERSION=v2.14.0
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
+3 -3
View File
@@ -1,12 +1,12 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.10
ARG GO_VERSION=1.27.3
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.23
ARG MODOUTDATED_VERSION=v0.8.0
ARG ALPINE_VERSION=3.24
ARG MODOUTDATED_VERSION=v0.9.0
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
ENV GOTOOLCHAIN=local
@@ -44,8 +44,8 @@ created. Supported `Dockerfile` instructions:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
$ docker commit c3f279d17e0a svendowideit/testimage:version3
@@ -63,8 +63,8 @@ svendowideit/testimage version3 f5283438590d 16 sec
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
$ docker inspect -f "{{ .Config.Env }}" c3f279d17e0a
@@ -85,8 +85,8 @@ $ docker inspect -f "{{ .Config.Env }}" f5283438590d
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
$ docker commit --change='CMD ["apachectl", "-DFOREGROUND"]' -c "EXPOSE 80" c3f279d17e0a svendowideit/testimage:version4
@@ -100,6 +100,6 @@ $ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
89373736e2e7 testimage:version4 "apachectl -DFOREGROU" 3 seconds ago Up 2 seconds 80/tcp distracted_fermat
c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
c3f279d17e0a ubuntu:26.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:26.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
```
+5 -5
View File
@@ -33,7 +33,7 @@ Running `docker ps --no-trunc` showing 2 linked containers.
$ docker ps --no-trunc
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ca5534a51dd04bbcebe9b23ba05f389466cf0c190f1f8f182d7eea92a9671d00 ubuntu:24.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp
ca5534a51dd04bbcebe9b23ba05f389466cf0c190f1f8f182d7eea92a9671d00 ubuntu:26.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp
9ca9747b233100676a48cc7806131586213fa5dab86dd1972d6a8732e3a84a4d crosbymichael/redis:latest /redis-server --dir 33 minutes ago Up 33 minutes 6379/tcp redis,webapp/db
```
@@ -240,13 +240,13 @@ CONTAINER ID IMAGE COMMAND CREATED
919e1179bdb8 ubuntu-c1 "top" About a minute ago Up About a minute admiring_lovelace
```
Match containers based on the `ubuntu` version `24.04` image:
Match containers based on the `ubuntu` version `26.04` image:
```console
$ docker ps --filter ancestor=ubuntu:24.04
$ docker ps --filter ancestor=ubuntu:26.04
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
82a598284012 ubuntu:24.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
82a598284012 ubuntu:26.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
```
The following matches containers based on the layer `d0e008c6cf02` or an image
@@ -256,7 +256,7 @@ that have this layer in its layer stack.
$ docker ps --filter ancestor=d0e008c6cf02
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
82a598284012 ubuntu:24.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
82a598284012 ubuntu:26.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
```
#### Create time
+19 -19
View File
@@ -66,18 +66,18 @@ docker.io/library/debian:latest
Docker images can consist of multiple layers. In the example above, the image
consists of a single layer; `e756f3fdd6a3`.
Layers can be reused by images. For example, the `debian:bookworm` image shares
its layer with the `debian:latest`. Pulling the `debian:bookworm` image therefore
Layers can be reused by images. For example, the `debian:trixie` image shares
its layer with the `debian:latest`. Pulling the `debian:trixie` image therefore
only pulls its metadata, but not its layers, because the layer is already present
locally:
```console
$ docker image pull debian:bookworm
$ docker image pull debian:trixie
bookworm: Pulling from library/debian
trixie: Pulling from library/debian
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
Status: Downloaded newer image for debian:bookworm
docker.io/library/debian:bookworm
Status: Downloaded newer image for debian:trixie
docker.io/library/debian:trixie
```
To see which images are present locally, use the [`docker images`](image_ls.md)
@@ -87,13 +87,13 @@ command:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
debian bookworm 4eacea30377a 8 days ago 124MB
debian trixie 4eacea30377a 8 days ago 124MB
debian latest 4eacea30377a 8 days ago 124MB
```
Docker uses a content-addressable image store, and the image ID is a SHA256
digest covering the image's configuration and layers. In the example above,
`debian:bookworm` and `debian:latest` have the same image ID because they are
`debian:trixie` and `debian:latest` have the same image ID because they are
the same image tagged with different names. Because they are the same image,
their layers are stored only once and do not consume extra disk space.
@@ -106,8 +106,8 @@ refer to [understand images, containers, and storage drivers](https://docs.docke
So far, you've pulled images by their name (and "tag"). Using names and tags is
a convenient way to work with images. When using tags, you can `docker pull` an
image again to make sure you have the most up-to-date version of that image.
For example, `docker pull ubuntu:24.04` pulls the latest version of the Ubuntu
24.04 image.
For example, `docker pull ubuntu:26.04` pulls the latest version of the Ubuntu
26.04 image.
In some cases you don't want images to be updated to newer versions, but prefer
to use a fixed version of an image. Docker enables you to pull an image by its
@@ -116,16 +116,16 @@ of an image to pull. Doing so, allows you to "pin" an image to that version,
and guarantee that the image you're using is always the same.
To know the digest of an image, pull the image first. Let's pull the latest
`ubuntu:24.04` image from Docker Hub:
`ubuntu:26.04` image from Docker Hub:
```console
$ docker pull ubuntu:24.04
$ docker pull ubuntu:26.04
24.04: Pulling from library/ubuntu
26.04: Pulling from library/ubuntu
125a6e411906: Pull complete
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:24.04
Status: Downloaded newer image for ubuntu:26.04
docker.io/library/ubuntu:26.04
```
Docker prints the digest of the image after the pull has finished. In the example
@@ -213,11 +213,11 @@ shorthand) to see the images that were pulled. The example below shows all the
```console
$ docker image ls --filter reference=ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 22.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu jammy 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu 24.04 35a88802559d 6 weeks ago 78.1MB
ubuntu 24.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu noble 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu 26.04 35a88802559d 6 weeks ago 78.1MB
ubuntu latest 35a88802559d 6 weeks ago 78.1MB
ubuntu noble 35a88802559d 6 weeks ago 78.1MB
ubuntu resolute 35a88802559d 6 weeks ago 78.1MB
```
### Cancel a pull
-12
View File
@@ -70,18 +70,6 @@ With Docker Desktop, the credential store is already installed and configured
for you. Unless you want to change the credential store used by Docker Desktop,
you can skip the following steps.
#### Docker Desktop sign-in
When Docker Desktop is running and you are signed in, commands that pull or
push images through the Docker Engine, such as `docker pull`, `docker push`,
`docker run`, and `docker service create`, use the Docker Desktop session for
Docker Hub (`docker.io`) and Docker Hardened Images (`dhi.io`). The session is
used before the credential store, credential helpers, and the configuration
file. Credentials set in the `DOCKER_AUTH_CONFIG` environment variable take
precedence over the Docker Desktop session. If Docker Desktop is not installed
or not running, or you are not signed in, the Docker CLI uses its own
credentials.
#### Configure the credential store
You need to specify the credential store in `$HOME/.docker/config.json`
+1 -1
View File
@@ -706,7 +706,7 @@ follows:
| `node.platform.os` | Node operating system | `node.platform.os==windows` |
| `node.platform.arch` | Node architecture | `node.platform.arch==x86_64` |
| `node.labels` | User-defined node labels | `node.labels.security==high` |
| `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-24.04` |
| `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-26.04` |
`engine.labels` apply to Docker Engine labels like operating system, drivers,
etc. Swarm administrators add `node.labels` for operational purposes by using
+2 -2
View File
@@ -339,8 +339,8 @@ $ docker events --filter 'type=network'
$ docker events --filter 'container=container_1' --filter 'container=container_2'
2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:24.04)
2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:26.04)
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:26.04)
2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (imager=redis:7.2)
2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:7.2)
+19 -19
View File
@@ -35,7 +35,7 @@ reference to create or run a container based on an image.
An image tag is the image version, which defaults to `latest` when omitted. Use
the tag to run a container from specific version of an image. For example, to
run version `24.04` of the `ubuntu` image: `docker run ubuntu:24.04`.
run version `26.04` of the `ubuntu` image: `docker run ubuntu:26.04`.
#### Image digests
@@ -406,14 +406,14 @@ We have four ways to set user memory usage:
Examples:
```console
$ docker run -it ubuntu:24.04 /bin/bash
$ docker run -it ubuntu:26.04 /bin/bash
```
We set nothing about memory, this means the processes in the container can use
as much memory and swap memory as they need.
```console
$ docker run -it -m 300M --memory-swap -1 ubuntu:24.04 /bin/bash
$ docker run -it -m 300M --memory-swap -1 ubuntu:26.04 /bin/bash
```
We set memory limit and disabled swap memory limit, this means the processes in
@@ -421,7 +421,7 @@ the container can use 300M memory and as much swap memory as they need (if the
host supports swap memory).
```console
$ docker run -it -m 300M ubuntu:24.04 /bin/bash
$ docker run -it -m 300M ubuntu:26.04 /bin/bash
```
We set memory limit only, this means the processes in the container can use
@@ -430,7 +430,7 @@ We set memory limit only, this means the processes in the container can use
would be 2*300M, so processes can use 300M swap memory as well.
```console
$ docker run -it -m 300M --memory-swap 1G ubuntu:24.04 /bin/bash
$ docker run -it -m 300M --memory-swap 1G ubuntu:26.04 /bin/bash
```
We set both memory and swap memory, so the processes in the container can use
@@ -456,7 +456,7 @@ The following example limits the memory (`-m`) to 500M and sets the memory
reservation to 200M.
```console
$ docker run -it -m 500M --memory-reservation 200M ubuntu:24.04 /bin/bash
$ docker run -it -m 500M --memory-reservation 200M ubuntu:26.04 /bin/bash
```
Under this configuration, when the container consumes memory more than 200M and
@@ -466,7 +466,7 @@ memory below 200M.
The following example set memory reservation to 1G without a hard memory limit.
```console
$ docker run -it --memory-reservation 1G ubuntu:24.04 /bin/bash
$ docker run -it --memory-reservation 1G ubuntu:26.04 /bin/bash
```
The container can use as much memory as it needs. The memory reservation setting
@@ -484,13 +484,13 @@ The following example limits the memory to 100M and disables the OOM killer for
this container:
```console
$ docker run -it -m 100M --oom-kill-disable ubuntu:24.04 /bin/bash
$ docker run -it -m 100M --oom-kill-disable ubuntu:26.04 /bin/bash
```
The following example, illustrates a dangerous way to use the flag:
```console
$ docker run -it --oom-kill-disable ubuntu:24.04 /bin/bash
$ docker run -it --oom-kill-disable ubuntu:26.04 /bin/bash
```
The container has unlimited memory which can cause the host to run out memory
@@ -510,7 +510,7 @@ between 0 and 100. A value of 0 turns off anonymous page swapping. A value of
For example, you can set:
```console
$ docker run -it --memory-swappiness=0 ubuntu:24.04 /bin/bash
$ docker run -it --memory-swappiness=0 ubuntu:26.04 /bin/bash
```
Setting the `--memory-swappiness` option is helpful when you want to retain the
@@ -561,7 +561,7 @@ And usually `--cpu-period` should work with `--cpu-quota`.
Examples:
```console
$ docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu:24.04 /bin/bash
$ docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu:26.04 /bin/bash
```
If there is 1 CPU, this means the container can get 50% CPU worth of run-time every 50ms.
@@ -582,13 +582,13 @@ We can set cpus in which to allow execution for containers.
Examples:
```console
$ docker run -it --cpuset-cpus="1,3" ubuntu:24.04 /bin/bash
$ docker run -it --cpuset-cpus="1,3" ubuntu:26.04 /bin/bash
```
This means processes in container can be executed on cpu 1 and cpu 3.
```console
$ docker run -it --cpuset-cpus="0-2" ubuntu:24.04 /bin/bash
$ docker run -it --cpuset-cpus="0-2" ubuntu:26.04 /bin/bash
```
This means processes in container can be executed on cpu 0, cpu 1 and cpu 2.
@@ -599,14 +599,14 @@ on NUMA systems.
Examples:
```console
$ docker run -it --cpuset-mems="1,3" ubuntu:24.04 /bin/bash
$ docker run -it --cpuset-mems="1,3" ubuntu:26.04 /bin/bash
```
This example restricts the processes in the container to only use memory from
memory nodes 1 and 3.
```console
$ docker run -it --cpuset-mems="0-2" ubuntu:24.04 /bin/bash
$ docker run -it --cpuset-mems="0-2" ubuntu:26.04 /bin/bash
```
This example restricts the processes in the container to only use memory from
@@ -637,8 +637,8 @@ For example, the commands below create two containers with different blkio
weight:
```console
$ docker run -it --name c1 --blkio-weight 300 ubuntu:24.04 /bin/bash
$ docker run -it --name c2 --blkio-weight 600 ubuntu:24.04 /bin/bash
$ docker run -it --name c1 --blkio-weight 300 ubuntu:26.04 /bin/bash
$ docker run -it --name c2 --blkio-weight 600 ubuntu:26.04 /bin/bash
```
If you do block IO in the two containers at the same time, by, for example:
@@ -854,11 +854,11 @@ For interacting with the network stack, instead of using `--privileged` they
should use `--cap-add=NET_ADMIN` to modify the network interfaces.
```console
$ docker run -it --rm ubuntu:24.04 ip link add dummy0 type dummy
$ docker run -it --rm ubuntu:26.04 ip link add dummy0 type dummy
RTNETLINK answers: Operation not permitted
$ docker run -it --rm --cap-add=NET_ADMIN ubuntu:24.04 ip link add dummy0 type dummy
$ docker run -it --rm --cap-add=NET_ADMIN ubuntu:26.04 ip link add dummy0 type dummy
```
To mount a FUSE based filesystem, you need to combine both `--cap-add` and
+2 -3
View File
@@ -51,8 +51,7 @@ const (
// IndexServer is used for user auth and image search
IndexServer = "https://index.docker.io/v1/"
// IndexName is the name of the index
IndexName = "docker.io"
DefaultRegistryHost = "registry-1.docker.io"
IndexName = "docker.io"
)
var (
@@ -62,7 +61,7 @@ var (
// or "ubuntu:latest"), as well as references using the "docker.io" domain
// name, which is used as canonical reference for images on Docker Hub, but
// does not match the domain-name of Docker Hub's registry.
DefaultV2Registry = &url.URL{Scheme: "https", Host: DefaultRegistryHost}
DefaultV2Registry = &url.URL{Scheme: "https", Host: "registry-1.docker.io"}
validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`)
-62
View File
@@ -1,62 +0,0 @@
package registryauth
import (
"os"
"github.com/distribution/reference"
"github.com/docker/cli/cli/config/configfile"
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/internal/secretsengine"
"github.com/moby/moby/api/pkg/authconfig"
registrytypes "github.com/moby/moby/api/types/registry"
)
type configProvider interface {
ConfigFile() *configfile.ConfigFile
}
type authProvider interface {
RegistryAuthProvider() secretsengine.AuthProvider
}
type envAuthFunc func(registryHostname string) (configtypes.AuthConfig, bool)
func EncodedAuth(dockerCLI configProvider, image string) (string, error) {
var auth secretsengine.AuthProvider = dockerCLI.ConfigFile()
if p, ok := dockerCLI.(authProvider); ok {
auth = p.RegistryAuthProvider()
}
return encodedAuth(envAuthConfig, auth, image)
}
func encodedAuth(envAuth envAuthFunc, auth secretsengine.AuthProvider, image string) (string, error) {
registryRef, err := reference.ParseNormalizedNamed(image)
if err != nil {
return "", err
}
domain := reference.Domain(registryRef)
authConfig, ok := envAuth(domain)
if !ok {
authConfig, err = auth.GetAuthConfig(domain)
if err != nil {
return "", err
}
}
return authconfig.Encode(registrytypes.AuthConfig{
Username: authConfig.Username,
Password: authConfig.Password,
ServerAddress: authConfig.ServerAddress,
Auth: authConfig.Auth,
IdentityToken: authConfig.IdentityToken,
RegistryToken: authConfig.RegistryToken,
})
}
func envAuthConfig(registryHostname string) (configtypes.AuthConfig, bool) {
if os.Getenv(configfile.DockerEnvConfigKey) == "" {
return configtypes.AuthConfig{}, false
}
authConfig, err := configfile.New("").GetAuthConfig(registryHostname)
return authConfig, err == nil && authConfig.Username != ""
}
-131
View File
@@ -1,131 +0,0 @@
package registryauth
import (
"bytes"
"sync"
"testing"
"github.com/docker/cli/cli/config/configfile"
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/internal/secretsengine"
"github.com/moby/moby/api/pkg/authconfig"
registrytypes "github.com/moby/moby/api/types/registry"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
type recordingAuthProvider struct {
mu sync.Mutex
hosts []string
}
func (p *recordingAuthProvider) GetAuthConfig(registryHostname string) (configtypes.AuthConfig, error) {
p.mu.Lock()
defer p.mu.Unlock()
p.hosts = append(p.hosts, registryHostname)
return configtypes.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: registryHostname}, nil
}
type configOnlyCli struct {
cfg *configfile.ConfigFile
}
func (c *configOnlyCli) ConfigFile() *configfile.ConfigFile {
return c.cfg
}
type fakeCli struct {
configOnlyCli
auth *recordingAuthProvider
}
func (c *fakeCli) RegistryAuthProvider() secretsengine.AuthProvider {
return c.auth
}
func noEnvAuth(string) (configtypes.AuthConfig, bool) {
return configtypes.AuthConfig{}, false
}
func loadConfig(t *testing.T) *configfile.ConfigFile {
t.Helper()
const configFileContent = `{"auths": {
"https://index.docker.io/v1/": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
"localhost:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}
}
}`
cfg := &configfile.ConfigFile{}
assert.NilError(t, cfg.LoadFromReader(bytes.NewReader([]byte(configFileContent))))
return cfg
}
func TestEncodedAuthUsesCLIAuthProvider(t *testing.T) {
t.Parallel()
dockerCLI := &fakeCli{configOnlyCli: configOnlyCli{cfg: configfile.New("")}, auth: &recordingAuthProvider{}}
actual, err := EncodedAuth(dockerCLI, "dhi.io/python")
assert.NilError(t, err)
expected, err := authconfig.Encode(registrytypes.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: "dhi.io"})
assert.NilError(t, err)
assert.Check(t, is.Equal(actual, expected))
assert.Check(t, is.DeepEqual(dockerCLI.auth.hosts, []string{"dhi.io"}))
}
func TestEncodedAuthWithoutAuthProviderUsesConfigFile(t *testing.T) {
t.Parallel()
actual, err := EncodedAuth(&configOnlyCli{cfg: loadConfig(t)}, "ubuntu")
assert.NilError(t, err)
expected, err := authconfig.Encode(registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"})
assert.NilError(t, err)
assert.Check(t, is.Equal(actual, expected))
}
func TestEncodedAuthEnvTakesPrecedence(t *testing.T) {
t.Parallel()
envAuth := func(registryHostname string) (configtypes.AuthConfig, bool) {
return configtypes.AuthConfig{Username: "envuser", Password: "envpass", ServerAddress: registryHostname}, true
}
auth := &recordingAuthProvider{}
actual, err := encodedAuth(envAuth, auth, "ubuntu")
assert.NilError(t, err)
expected, err := authconfig.Encode(registrytypes.AuthConfig{Username: "envuser", Password: "envpass", ServerAddress: "docker.io"})
assert.NilError(t, err)
assert.Check(t, is.Equal(actual, expected))
assert.Check(t, is.Len(auth.hosts, 0), "the auth provider must not be asked for a registry in DOCKER_AUTH_CONFIG")
}
func TestEnvAuthConfig(t *testing.T) {
t.Setenv(configfile.DockerEnvConfigKey, `{"auths":{"https://index.docker.io/v1/":{"auth":"ZW52dXNlcjplbnZwYXNz"}}}`)
got, ok := envAuthConfig("docker.io")
assert.Check(t, ok)
assert.Check(t, is.DeepEqual(got, configtypes.AuthConfig{Username: "envuser", Password: "envpass", ServerAddress: "https://index.docker.io/v1/"}))
_, ok = envAuthConfig("ghcr.io")
assert.Check(t, !ok)
}
func TestEncodedAuthFromConfigFile(t *testing.T) {
t.Parallel()
cfg := loadConfig(t)
tests := []struct {
image string
expected registrytypes.AuthConfig
}{
{image: "ubuntu", expected: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"}},
{image: "localhost:5000/ubuntu", expected: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "localhost:5000"}},
{image: "no-auth.example.com/ubuntu", expected: registrytypes.AuthConfig{}},
}
for _, tc := range tests {
t.Run(tc.image, func(t *testing.T) {
t.Parallel()
actual, err := encodedAuth(noEnvAuth, cfg, tc.image)
assert.NilError(t, err)
expected, err := authconfig.Encode(tc.expected)
assert.NilError(t, err)
assert.Check(t, is.Equal(actual, expected))
})
}
}
+2 -2
View File
@@ -69,10 +69,10 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API
// get the http transport, this will be used in a client to upload manifest
base := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: endpoint.TLSConfig,
DisableKeepAlives: true,
-199
View File
@@ -1,199 +0,0 @@
//go:build go1.26
package secretsengine
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"sync"
"time"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types"
"github.com/docker/cli/internal/registry"
seclient "github.com/docker/secrets-engine/client"
"github.com/docker/secrets-engine/client/dockerhub"
"github.com/docker/secrets-engine/x/api"
"github.com/sirupsen/logrus"
)
const (
lookupTimeout = 5 * time.Second
sessionExpirySkew = 30 * time.Second
sessionTTL = time.Minute
retryInterval = time.Minute
stagingRegistryHost = "registry-1-stage.docker.io"
dhiRegistryHost = "dhi.io"
)
var errSessionExpired = errors.New("the session has expired (not signed in to Docker Desktop?)")
type hubClient interface {
GetDefaultSession(ctx context.Context) (dockerhub.UserSession, error)
GetDefaultProfile(ctx context.Context) (dockerhub.Profile, error)
}
var _ hubClient = dockerhub.ClientAuth(nil)
type AuthProvider interface {
GetAuthConfig(registryHostname string) (types.AuthConfig, error)
}
type provider struct {
fallback AuthProvider
hub *session
staging *session
}
func NewDesktop(fallback AuthProvider) AuthProvider {
return &provider{
fallback: fallback,
hub: newDesktopSession("Docker Hub"),
staging: newDesktopSession("Docker Hub staging", dockerhub.Staging()),
}
}
func (p *provider) GetAuthConfig(registryHostname string) (types.AuthConfig, error) {
if s, serverAddress := p.sessionFor(registryHostname); s != nil {
if auth, ok := s.auth(); ok {
auth.ServerAddress = serverAddress
return auth, nil
}
}
return p.fallback.GetAuthConfig(registryHostname)
}
func (p *provider) sessionFor(registryHostname string) (*session, string) {
switch credentials.ConvertToHostname(registryHostname) {
case registry.IndexName, registry.IndexHostname, registry.DefaultRegistryHost:
return p.hub, registry.IndexServer
case dhiRegistryHost:
return p.hub, dhiRegistryHost
case stagingRegistryHost:
return p.staging, stagingRegistryHost
default:
return nil, ""
}
}
type session struct {
name string
connect func() (hubClient, error)
mu sync.Mutex
hub hubClient
retryAt time.Time
cached *types.AuthConfig
validUntil time.Time
}
func newDesktopSession(name string, opts ...dockerhub.Option) *session {
return &session{
name: name,
connect: func() (hubClient, error) {
return connect(api.DesktopSocketPath(), opts...)
},
}
}
func connect(socketPath string, opts ...dockerhub.Option) (hubClient, error) {
if _, err := os.Lstat(socketPath); errors.Is(err, fs.ErrNotExist) {
return nil, err
}
c, err := seclient.New(
seclient.WithSocketPath(socketPath),
seclient.WithTimeout(lookupTimeout),
)
if err != nil {
return nil, err
}
return c.HubAuth(opts...), nil
}
func (s *session) auth() (types.AuthConfig, bool) {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
if now.Before(s.retryAt) {
return types.AuthConfig{}, false
}
if s.cached != nil && now.Before(s.validUntil) {
return *s.cached, true
}
auth, validUntil, err := s.fetch()
if err != nil {
s.retryAt = time.Now().Add(retryInterval)
s.cached = nil
logFallback(s.name, err)
return types.AuthConfig{}, false
}
logrus.Debugf("using the %s session of %q from Docker Desktop", s.name, auth.Username)
s.cached, s.validUntil = &auth, validUntil
return auth, true
}
func (s *session) fetch() (types.AuthConfig, time.Time, error) {
if s.hub == nil {
hub, err := s.connect()
if err != nil {
return types.AuthConfig{}, time.Time{}, err
}
s.hub = hub
}
ctx, cancel := context.WithTimeout(context.Background(), lookupTimeout)
defer cancel()
userSession, err := s.hub.GetDefaultSession(ctx)
if err != nil {
return types.AuthConfig{}, time.Time{}, err
}
username := userSession.Claims.Username
if username == "" {
profile, err := s.hub.GetDefaultProfile(ctx)
if err != nil {
return types.AuthConfig{}, time.Time{}, fmt.Errorf("resolving the Docker Hub username: %w", err)
}
username = profile.Username
}
if username == "" {
return types.AuthConfig{}, time.Time{}, errors.New("no username for the session")
}
now := time.Now()
validUntil := now.Add(sessionTTL)
if exp := userSession.Claims.ExpiresAt; exp != nil {
validUntil = exp.Add(-sessionExpirySkew)
if !now.Before(validUntil) {
return types.AuthConfig{}, time.Time{}, errSessionExpired
}
}
return types.AuthConfig{
Username: username,
Password: userSession.AccessToken,
}, validUntil, nil
}
func logFallback(name string, err error) {
if isSessionUnavailable(err) {
logrus.Debugf("no %s session available from Docker Desktop, using Docker CLI credentials: %v", name, err)
return
}
logrus.Warnf("could not use the %s session from Docker Desktop, using Docker CLI credentials instead: %v", name, err)
}
func isSessionUnavailable(err error) bool {
if _, ok := errors.AsType[*seclient.ConnectError](err); ok {
return true
}
return errors.Is(err, fs.ErrNotExist) ||
errors.Is(err, dockerhub.ErrNoSession) ||
errors.Is(err, errSessionExpired)
}
@@ -1,429 +0,0 @@
//go:build go1.26
package secretsengine
import (
"context"
"errors"
"fmt"
"io/fs"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"testing/synctest"
"time"
"github.com/docker/cli/cli/config/types"
"github.com/docker/cli/internal/registry"
seclient "github.com/docker/secrets-engine/client"
"github.com/docker/secrets-engine/client/dockerhub"
"gotest.tools/v3/assert"
)
type fakeHub struct {
session dockerhub.UserSession
sessionErr error
profile dockerhub.Profile
profileErr error
sessionCalls atomic.Int32
}
func (f *fakeHub) GetDefaultSession(context.Context) (dockerhub.UserSession, error) {
f.sessionCalls.Add(1)
return f.session, f.sessionErr
}
func (f *fakeHub) GetDefaultProfile(context.Context) (dockerhub.Profile, error) {
return f.profile, f.profileErr
}
func hubSession(username string, expiresAt time.Time) dockerhub.UserSession {
s := dockerhub.UserSession{AccessToken: "desktop-oauth-token"}
s.Claims.Username = username
if !expiresAt.IsZero() {
s.Claims.ExpiresAt = &dockerhub.NumericDate{Time: expiresAt}
}
return s
}
func inAnHour() time.Time {
return time.Now().Add(time.Hour)
}
type recordingFallback struct {
mu sync.Mutex
hosts []string
}
func (f *recordingFallback) GetAuthConfig(registryHostname string) (types.AuthConfig, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.hosts = append(f.hosts, registryHostname)
return cliCredentials(registryHostname), nil
}
func cliCredentials(registryHostname string) types.AuthConfig {
return types.AuthConfig{Username: "cli-user", Password: "cli-password", ServerAddress: registryHostname}
}
func desktopCredentials() types.AuthConfig {
return types.AuthConfig{Username: "hubuser", Password: "desktop-oauth-token", ServerAddress: registry.IndexServer}
}
func stagingCredentials() types.AuthConfig {
return types.AuthConfig{Username: "stageuser", Password: "staging-oauth-token", ServerAddress: stagingRegistryHost}
}
type testProvider struct {
*provider
fallback *recordingFallback
staging *fakeHub
}
func newTestProvider(hub hubClient) *testProvider {
tp := &testProvider{
fallback: &recordingFallback{},
staging: &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile},
}
tp.provider = &provider{
fallback: tp.fallback,
hub: &session{name: "Docker Hub", hub: hub},
staging: &session{name: "Docker Hub staging", hub: tp.staging},
}
return tp
}
func (tp *testProvider) signInToStaging() {
tp.staging.sessionErr = nil
tp.staging.session = hubSession("stageuser", inAnHour())
tp.staging.session.AccessToken = "staging-oauth-token"
}
func connectError(reason seclient.ConnectReason) error {
return &seclient.ConnectError{
Reason: reason,
SocketPath: "/run/engine.sock",
Err: errors.New("dial unix /run/engine.sock"),
}
}
func TestNewDesktopUsesSeparateSessions(t *testing.T) {
t.Parallel()
p := NewDesktop(&recordingFallback{}).(*provider)
assert.Assert(t, p.hub != p.staging, "production and staging sessions must be separate")
}
func TestDockerHubUsesDesktopSession(t *testing.T) {
t.Parallel()
for _, host := range []string{"docker.io", "index.docker.io", "registry-1.docker.io", registry.IndexServer} {
t.Run(host, func(t *testing.T) {
t.Parallel()
hub := &fakeHub{session: hubSession("hubuser", inAnHour())}
p := newTestProvider(hub)
got, err := p.GetAuthConfig(host)
assert.NilError(t, err)
assert.DeepEqual(t, got, desktopCredentials())
assert.Equal(t, got.IdentityToken, "", "an IdentityToken is sent as a refresh token")
assert.Equal(t, len(p.fallback.hosts), 0)
})
}
}
func TestOtherRegistriesUseFallback(t *testing.T) {
t.Parallel()
hosts := []string{
"ghcr.io", "localhost:5000", "registry.example.com", "",
stagingRegistryHost + ".example.com", "example.com/" + stagingRegistryHost,
dhiRegistryHost + ".example.com", "example.com/" + dhiRegistryHost, "registry." + dhiRegistryHost,
"docker.io.example.com",
}
for _, host := range hosts {
t.Run(host, func(t *testing.T) {
t.Parallel()
hub := &fakeHub{session: hubSession("hubuser", inAnHour())}
p := newTestProvider(hub)
p.signInToStaging()
got, err := p.GetAuthConfig(host)
assert.NilError(t, err)
assert.DeepEqual(t, got, cliCredentials(host))
assert.DeepEqual(t, p.fallback.hosts, []string{host})
assert.Equal(t, hub.sessionCalls.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub")
assert.Equal(t, p.staging.sessionCalls.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub")
})
}
}
func TestDHIUsesDockerHubSession(t *testing.T) {
t.Parallel()
for _, host := range []string{dhiRegistryHost, "https://" + dhiRegistryHost, "https://" + dhiRegistryHost + "/"} {
t.Run(host, func(t *testing.T) {
t.Parallel()
hub := &fakeHub{session: hubSession("hubuser", inAnHour())}
p := newTestProvider(hub)
p.signInToStaging()
want := desktopCredentials()
want.ServerAddress = dhiRegistryHost
got, err := p.GetAuthConfig(host)
assert.NilError(t, err)
assert.DeepEqual(t, got, want)
assert.Equal(t, p.staging.sessionCalls.Load(), int32(0), "the staging session must not be read for DHI")
got, err = p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, desktopCredentials())
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
})
}
}
func TestStagingRegistryUsesStagingSession(t *testing.T) {
t.Parallel()
for _, host := range []string{stagingRegistryHost, "https://" + stagingRegistryHost + "/"} {
t.Run(host, func(t *testing.T) {
t.Parallel()
hub := &fakeHub{session: hubSession("hubuser", inAnHour())}
p := newTestProvider(hub)
p.signInToStaging()
got, err := p.GetAuthConfig(host)
assert.NilError(t, err)
assert.DeepEqual(t, got, stagingCredentials())
assert.Equal(t, hub.sessionCalls.Load(), int32(0), "the production session must not be read for the staging registry")
})
}
}
func TestEnvironmentsDoNotMix(t *testing.T) {
t.Parallel()
t.Run("staging session is not used for docker.io", func(t *testing.T) {
t.Parallel()
p := newTestProvider(&fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile})
p.signInToStaging()
got, err := p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, cliCredentials("docker.io"))
got, err = p.GetAuthConfig(stagingRegistryHost)
assert.NilError(t, err)
assert.DeepEqual(t, got, stagingCredentials())
})
t.Run("production session is not used for the staging registry", func(t *testing.T) {
t.Parallel()
p := newTestProvider(&fakeHub{session: hubSession("hubuser", inAnHour())})
got, err := p.GetAuthConfig(stagingRegistryHost)
assert.NilError(t, err)
assert.DeepEqual(t, got, cliCredentials(stagingRegistryHost))
got, err = p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, desktopCredentials())
})
}
func TestUsernameFromProfile(t *testing.T) {
t.Parallel()
hub := &fakeHub{
session: hubSession("", inAnHour()),
profile: dockerhub.Profile{Username: "hubuser"},
}
p := newTestProvider(hub)
got, err := p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, desktopCredentials())
}
func TestFallsBackWithoutUsableSession(t *testing.T) {
t.Parallel()
tests := []struct {
name string
hub func() *fakeHub
}{
{name: "secrets engine not running", hub: func() *fakeHub { return &fakeHub{sessionErr: connectError(seclient.ReasonNotRunning)} }},
{name: "secrets engine socket not accessible", hub: func() *fakeHub { return &fakeHub{sessionErr: connectError(seclient.ReasonPermissionDenied)} }},
{name: "nobody signed in", hub: func() *fakeHub { return &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile} }},
{name: "access denied", hub: func() *fakeHub { return &fakeHub{sessionErr: seclient.ErrAccessDenied} }},
{name: "no username", hub: func() *fakeHub { return &fakeHub{session: hubSession("", inAnHour())} }},
{name: "profile lookup fails", hub: func() *fakeHub {
return &fakeHub{session: hubSession("", inAnHour()), profileErr: errors.New("boom")}
}},
{name: "expired", hub: func() *fakeHub { return &fakeHub{session: hubSession("hubuser", time.Now().Add(-time.Minute))} }},
{name: "expires within the skew", hub: func() *fakeHub {
return &fakeHub{session: hubSession("hubuser", time.Now().Add(sessionExpirySkew/2))}
}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
hub := tc.hub()
p := newTestProvider(hub)
for range 2 {
got, err := p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, cliCredentials("docker.io"))
}
assert.Equal(t, hub.sessionCalls.Load(), int32(1), "Desktop must not be asked again within retryInterval")
})
})
}
}
func TestDesktopFallsBackWhenClientCannotBeCreated(t *testing.T) {
t.Parallel()
var connects atomic.Int32
s := &session{
name: "Docker Hub",
connect: func() (hubClient, error) {
connects.Add(1)
return nil, errors.New("no socket path")
},
}
p := &provider{fallback: &recordingFallback{}, hub: s}
for range 2 {
got, err := p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, cliCredentials("docker.io"))
}
assert.Equal(t, connects.Load(), int32(1))
}
func TestRetriesAfterInterval(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
hub := &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile}
p := newTestProvider(hub)
got, err := p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, cliCredentials("docker.io"))
hub.sessionErr = nil
hub.session = hubSession("hubuser", inAnHour())
time.Sleep(retryInterval - time.Second)
got, err = p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, cliCredentials("docker.io"))
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
time.Sleep(time.Second)
got, err = p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, desktopCredentials())
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
})
}
func TestReusesSessionUntilExpiry(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
expiresAt := time.Now().Add(10 * time.Minute)
hub := &fakeHub{session: hubSession("hubuser", expiresAt)}
p := newTestProvider(hub)
got, err := p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, desktopCredentials())
time.Sleep(5 * time.Minute)
got, err = p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, desktopCredentials())
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
hub.session = hubSession("hubuser", expiresAt.Add(time.Hour))
hub.session.AccessToken = "refreshed-token"
time.Sleep(5*time.Minute - sessionExpirySkew)
got, err = p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.Equal(t, got.Password, "refreshed-token")
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
})
}
func TestRefetchesSessionWithoutExpiry(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
hub := &fakeHub{session: hubSession("hubuser", time.Time{})}
p := newTestProvider(hub)
got, err := p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, desktopCredentials())
time.Sleep(sessionTTL - time.Second)
got, err = p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, desktopCredentials())
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
time.Sleep(time.Second)
got, err = p.GetAuthConfig("docker.io")
assert.NilError(t, err)
assert.DeepEqual(t, got, desktopCredentials())
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
})
}
func TestConcurrentLookupsShareOneFetch(t *testing.T) {
t.Parallel()
hub := &fakeHub{session: hubSession("hubuser", inAnHour())}
p := newTestProvider(hub)
const lookups = 20
results := make([]types.AuthConfig, lookups)
var wg sync.WaitGroup
for i := range lookups {
wg.Go(func() {
auth, err := p.GetAuthConfig("docker.io")
assert.Check(t, err)
results[i] = auth
})
}
wg.Wait()
for _, auth := range results {
assert.DeepEqual(t, auth, desktopCredentials())
}
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
}
func TestConnectSkipsMissingSocket(t *testing.T) {
t.Parallel()
_, err := connect(filepath.Join(t.TempDir(), "engine.sock"))
assert.Assert(t, errors.Is(err, fs.ErrNotExist), err)
assert.Assert(t, isSessionUnavailable(err))
}
func TestIsSessionUnavailable(t *testing.T) {
t.Parallel()
for _, err := range []error{
connectError(seclient.ReasonNotRunning),
fmt.Errorf("get session: %w", connectError(seclient.ReasonNotRunning)),
connectError(seclient.ReasonPermissionDenied),
connectError(seclient.ReasonTimeout),
connectError(seclient.ReasonUnknown),
dockerhub.ErrNoSession,
dockerhub.ErrNoDefaultProfile,
errSessionExpired,
fs.ErrNotExist,
} {
assert.Assert(t, isSessionUnavailable(err), err)
}
for _, err := range []error{
seclient.ErrAccessDenied,
errors.New("no username for the session"),
context.DeadlineExceeded,
} {
assert.Assert(t, !isSessionUnavailable(err), err)
}
}
-5
View File
@@ -12,7 +12,6 @@ import (
manifeststore "github.com/docker/cli/cli/manifest/store"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/registryclient"
"github.com/docker/cli/internal/secretsengine"
"github.com/moby/moby/client"
)
@@ -177,10 +176,6 @@ func (c *FakeCli) SetRegistryClient(registryClient registryclient.RegistryClient
c.registryClient = registryClient
}
func (c *FakeCli) RegistryAuthProvider() secretsengine.AuthProvider {
return c.configfile
}
// BuildKitEnabled on the fake cli
func (*FakeCli) BuildKitEnabled() (bool, error) {
return true, nil
+6 -3
View File
@@ -27,8 +27,13 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
}))
cli.SetOut(outStream)
r, _, err := os.Pipe()
r, w, err := os.Pipe()
assert.NilError(t, err)
t.Cleanup(func() {
_ = w.Close()
_ = r.Close()
})
cli.SetIn(streams.NewIn(r))
notifyCtx, notifyCancel := context.WithCancel(ctx)
@@ -58,8 +63,6 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
}()
}
assert.Check(t, cli.OutBuffer().Len() > 0)
// a small delay to ensure the plugin is prompting
time.Sleep(100 * time.Microsecond)
+2 -2
View File
@@ -48,10 +48,10 @@ Valid placeholders for the Go template are listed below:
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a87ecb4f327c ubuntu:24.04 /bin/sh -c #(nop) MA 20 minutes ago Exit 0 desperate_brattain
a87ecb4f327c ubuntu:26.04 /bin/sh -c #(nop) MA 20 minutes ago Exit 0 desperate_brattain
01946d9d34d8 busybox /bin/sh -c #(nop) MA 33 minutes ago Exit 0 thirsty_bell
c1d3b0166030 acffc0358b9e /bin/sh -c yum -y up 2 weeks ago Exit 1 determined_torvalds
41d50ecd2f57 ubuntu:24.04 /bin/sh -c #(nop) MA 2 weeks ago Exit 0 drunk_pike
41d50ecd2f57 ubuntu:26.04 /bin/sh -c #(nop) MA 2 weeks ago Exit 0 drunk_pike
## Display only IDs of all containers, including non-running
+1 -1
View File
@@ -10,7 +10,7 @@ The title REPOSITORY for the first title may seem confusing. It is essentially
the image name. However, because you can tag a specific image, and multiple tags
(image instances) can be associated with a single name, the name is really a
repository for all tagged images of the same name. For example consider an image
called ubuntu. It may be tagged with 24.04 or 22.04, etc. to manage different
called ubuntu. It may be tagged with 26.04 or 24.04, etc. to manage different
versions.
## Filters
+19 -19
View File
@@ -27,17 +27,17 @@ Docker Engine uses the `:latest` tag as a default. This example pulls the
Docker images can consist of multiple layers. In the example above, the image
consists of a single layer; `e756f3fdd6a3`.
Layers can be reused by images. For example, the `debian:bookworm` image shares
its layer with the `debian:latest`. Pulling the `debian:bookworm` image therefore
Layers can be reused by images. For example, the `debian:trixie` image shares
its layer with the `debian:latest`. Pulling the `debian:trixie` image therefore
only pulls its metadata, but not its layers, because the layer is already present
locally:
$ docker image pull debian:bookworm
$ docker image pull debian:trixie
bookworm: Pulling from library/debian
trixie: Pulling from library/debian
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
Status: Downloaded newer image for debian:bookworm
docker.io/library/debian:bookworm
Status: Downloaded newer image for debian:trixie
docker.io/library/debian:trixie
To see which images are present locally, use the **docker-images(1)**
command:
@@ -45,12 +45,12 @@ command:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
debian bookworm 4eacea30377a 8 days ago 124MB
debian trixie 4eacea30377a 8 days ago 124MB
debian latest 4eacea30377a 8 days ago 124MB
Docker uses a content-addressable image store, and the image ID is a SHA256
digest covering the image's configuration and layers. In the example above,
`debian:bookworm` and `debian:latest` have the same image ID because they are
`debian:trixie` and `debian:latest` have the same image ID because they are
the *same* image tagged with different names. Because they are the same image,
their layers are stored only once and do not consume extra disk space.
@@ -64,8 +64,8 @@ in the online documentation.
So far, you've pulled images by their name (and "tag"). Using names and tags is
a convenient way to work with images. When using tags, you can `docker image pull` an
image again to make sure you have the most up-to-date version of that image.
For example, `docker image pull ubuntu:24.04` pulls the latest version of the Ubuntu
24.04 image.
For example, `docker image pull ubuntu:26.04` pulls the latest version of the Ubuntu
26.04 image.
In some cases you don't want images to be updated to newer versions, but prefer
to use a fixed version of an image. Docker enables you to pull an image by its
@@ -74,15 +74,15 @@ of an image to pull. Doing so, allows you to "pin" an image to that version,
and guarantee that the image you're using is always the same.
To know the digest of an image, pull the image first. Let's pull the latest
`ubuntu:24.04` image from Docker Hub:
`ubuntu:26.04` image from Docker Hub:
$ docker image pull ubuntu:24.04
$ docker image pull ubuntu:26.04
24.04: Pulling from library/ubuntu
26.04: Pulling from library/ubuntu
125a6e411906: Pull complete
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:24.04
Status: Downloaded newer image for ubuntu:26.04
docker.io/library/ubuntu:26.04
Docker prints the digest of the image after the pull has finished. In the example
above, the digest of the image is:
@@ -159,11 +159,11 @@ images that are present locally:
$ docker image ls --filter reference=ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 22.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu jammy 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu 24.04 35a88802559d 6 weeks ago 78.1MB
ubuntu 24.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu noble 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu 26.04 35a88802559d 6 weeks ago 78.1MB
ubuntu latest 35a88802559d 6 weeks ago 78.1MB
ubuntu noble 35a88802559d 6 weeks ago 78.1MB
ubuntu resolute 35a88802559d 6 weeks ago 78.1MB
## Cancel a pull
+9 -9
View File
@@ -62,8 +62,8 @@ The following example outputs all events that were generated in the last 3 minut
relative to the current time on the client machine:
# docker events --since '3m'
2015-05-12T11:51:30.999999999Z07:00 4386fb97867d: (from ubuntu:24.04) die
2015-05-12T15:52:12.999999999Z07:00 4386fb97867d: (from ubuntu:24.04) stop
2015-05-12T11:51:30.999999999Z07:00 4386fb97867d: (from ubuntu:26.04) die
2015-05-12T15:52:12.999999999Z07:00 4386fb97867d: (from ubuntu:26.04) stop
2015-05-12T15:53:45.999999999Z07:00 7805c1d35632: (from redis:7.2) die
2015-05-12T15:54:03.999999999Z07:00 7805c1d35632: (from redis:7.2) stop
@@ -97,21 +97,21 @@ Lines. For information about JSON Lines, refer to https://jsonlines.org .
## Filters
$ docker events --filter 'event=stop'
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:26.04)
2014-09-03T17:42:14.999999999Z07:00 container stop 7805c1d35632 (image=redis:7.2)
$ docker events --filter 'image=ubuntu:24.04'
2014-05-10T17:42:14.999999999Z07:00 container start 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container die 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:24.04)
$ docker events --filter 'image=ubuntu:26.04'
2014-05-10T17:42:14.999999999Z07:00 container start 4386fb97867d (image=ubuntu:26.04)
2014-05-10T17:42:14.999999999Z07:00 container die 4386fb97867d (image=ubuntu:26.04)
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:26.04)
$ docker events --filter 'container=7805c1d35632'
2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (image=redis:7.2)
2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image= redis:7.2)
$ docker events --filter 'container=7805c1d35632' --filter 'container=4386fb97867d'
2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:24.04)
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:24.04)
2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:26.04)
2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:26.04)
2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (image=redis:7.2)
2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:7.2)
+2 -2
View File
@@ -71,8 +71,8 @@ Server:
seccomp
Profile: builtin
cgroupns
Kernel Version: 5.15.0-25-generic
Operating System: Ubuntu 22.04 LTS
Kernel Version: 6.8.0-31-generic
Operating System: Ubuntu 24.04 LTS
OSType: linux
Architecture: x86_64
CPUs: 1
+15 -8
View File
@@ -88,13 +88,20 @@ if [ "$CGO_ENABLED" = "1" ] && [ "$(go env GOOS)" != "windows" ]; then
fi
export GO_BUILDMODE
GO_LDFLAGS="${GO_LDFLAGS:-}"
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.GitCommit=${GITCOMMIT}\""
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.BuildTime=${BUILDTIME}\""
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.Version=${VERSION}\""
if test -n "${PLATFORM}"; then
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.PlatformName=${PLATFORM}\""
fi
# Omit DWARF debug information to reduce binary size, but retain the symbol
# table for tools that inspect Go binaries. Pass GO_LDFLAGS="-w=0" to retain DWARF.
GO_LDFLAGS="-w ${GO_LDFLAGS:-}"
# Quotes are interpreted by Go's -ldflags parser, not the shell.
# shellcheck disable=SC2089
{
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.GitCommit=${GITCOMMIT}\""
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.BuildTime=${BUILDTIME}\""
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.Version=${VERSION}\""
if test -n "${PLATFORM}"; then
GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.PlatformName=${PLATFORM}\""
fi
}
if [ "$CGO_ENABLED" = "1" ] && [ "$GO_LINKMODE" = "static" ] && [ "$(go env GOOS)" = "linux" ]; then
GO_LDFLAGS="$GO_LDFLAGS -linkmode external -extldflags -static"
fi
@@ -128,7 +135,7 @@ if [ -n "$GO_STRIP" ]; then
# /usr/local/go/pkg/tool/linux_amd64/link: /usr/local/go/pkg/tool/linux_amd64/link: running strip failed: exit status 1
# llvm-strip: error: unsupported load command (cmd=0x5)
# more info: https://github.com/docker/cli/pull/3717
GO_LDFLAGS="$GO_LDFLAGS -s -w"
GO_LDFLAGS="$GO_LDFLAGS -s"
fi
export GO_LDFLAGS="$GO_LDFLAGS" # https://github.com/koalaman/shellcheck/issues/2064
+28 -30
View File
@@ -4,7 +4,7 @@ module github.com/docker/cli
// There is no 'go.mod' file, as that would imply opting in for all the rules
// around SemVer, which this repo cannot abide by as it uses CalVer.
go 1.26.10
go 1.26.0
tool (
github.com/cpuguy83/go-md2man/v2 // for scripts/docs/generate-man.sh
@@ -21,13 +21,11 @@ require (
github.com/distribution/reference v0.6.0
github.com/docker/cli-docs-tool v0.11.0
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker-credential-helpers v0.9.9
github.com/docker/docker-credential-helpers v0.9.12
github.com/docker/go-connections v0.8.2
github.com/docker/go-units v0.5.0
github.com/docker/secrets-engine/client v0.2.2
github.com/docker/secrets-engine/x v0.8.5
github.com/fvbommel/sortorder v1.2.1
github.com/go-jose/go-jose/v4 v4.1.5
github.com/go-jose/go-jose/v4 v4.1.7
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/gogo/protobuf v1.3.2
github.com/google/go-cmp v0.7.0
@@ -35,12 +33,12 @@ require (
github.com/google/uuid v1.6.0
github.com/mattn/go-runewidth v0.0.31
github.com/moby/go-archive v0.3.3
github.com/moby/moby/api v1.56.1
github.com/moby/moby/client v0.6.2
github.com/moby/moby/api v1.56.2-0.20261106011722-402d0a16f131
github.com/moby/moby/client v0.6.3-0.20261106011722-402d0a16f131
github.com/moby/patternmatcher v0.6.1
github.com/moby/swarmkit/v2 v2.1.2
github.com/moby/swarmkit/v2 v2.1.3
github.com/moby/sys/atomicwriter v0.1.0
github.com/moby/sys/capability v0.4.0
github.com/moby/sys/capability v0.5.0
github.com/moby/sys/sequential v0.7.0
github.com/moby/sys/signal v0.7.1
github.com/moby/sys/symlink v0.3.0
@@ -54,26 +52,25 @@ require (
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0
go.opentelemetry.io/otel v1.46.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0
go.opentelemetry.io/otel/metric v1.46.0
go.opentelemetry.io/otel/sdk v1.46.0
go.opentelemetry.io/otel/sdk/metric v1.46.0
go.opentelemetry.io/otel/trace v1.46.0
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/sync v0.23.0
golang.org/x/sys v0.48.0
golang.org/x/term v0.46.0
golang.org/x/text v0.42.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0
go.opentelemetry.io/otel v1.47.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0
go.opentelemetry.io/otel/metric v1.47.0
go.opentelemetry.io/otel/sdk v1.47.0
go.opentelemetry.io/otel/sdk/metric v1.47.0
go.opentelemetry.io/otel/trace v1.47.0
go.yaml.in/yaml/v3 v3.0.6
golang.org/x/sync v0.24.0
golang.org/x/sys v0.49.1-0.20261022125458-f5cb0306ac0e // includes fix for https://go.dev/issue/82324
golang.org/x/term v0.47.0
golang.org/x/text v0.43.0
gotest.tools/v3 v3.5.2
tags.cncf.io/container-device-interface v1.1.1
)
require (
connectrpc.com/connect v1.19.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
github.com/Azure/go-ansiterm v0.0.0-20261013165250-51075c6ea6bc // indirect
github.com/Microsoft/go-winio v0.6.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
@@ -90,7 +87,7 @@ require (
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/klauspost/compress v1.20.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/user v0.4.1 // indirect
github.com/moby/sys/userns v0.2.1 // indirect
@@ -102,13 +99,14 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.etcd.io/raft/v3 v3.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect
go.opentelemetry.io/otel/log v1.47.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/mod v0.42.0 // indirect
golang.org/x/net v0.62.0 // indirect
golang.org/x/time v0.16.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.12 // indirect
)
+56 -62
View File
@@ -1,11 +1,9 @@
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
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/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
github.com/Azure/go-ansiterm v0.0.0-20261013165250-51075c6ea6bc h1:58s5YzGPNAP7+e1uemaL0ax/NPSeJOHdRpRIowd+JDk=
github.com/Azure/go-ansiterm v0.0.0-20261013165250-51075c6ea6bc/go.mod h1:DMNF6Kxv+EcuUvVuNkvC3f+BjEEmc/Fn9aU+qiD8zpU=
github.com/Microsoft/go-winio v0.6.3 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4=
github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -37,8 +35,8 @@ github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQt
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.9 h1:BkydjIgZ46JnDbqyM2p2fc63KMw6y+KHL3Em/2AGJ7w=
github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/docker-credential-helpers v0.9.12 h1:hAgjJcMuC17DPf0Kl/HQSZQ7cxVo+wEA8Oo1SzkmqFg=
github.com/docker/docker-credential-helpers v0.9.12/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-events v0.1.0 h1:J8VX4H7Ta8mmBhv/K/24x1kosKOrEEnYmGjcdFHTOW0=
@@ -49,16 +47,12 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/secrets-engine/client v0.2.2 h1:aPvmlzsxNA+C+xgp2F+WiiDbCSAXHC591HscFFOvPOE=
github.com/docker/secrets-engine/client v0.2.2/go.mod h1:c3l/N+fA0kiy9S8BE3kKrO5hOZu4+SLYZK9Jx+jc2cw=
github.com/docker/secrets-engine/x v0.8.5 h1:P7D/YESMjpX18S4ratBgoeJkAFIxgsPxe093uFmgiGc=
github.com/docker/secrets-engine/x v0.8.5/go.mod h1:jQ5CXCYCPn8Sw96TS8Ufq3oyIGH2GzUtVXIdwa2Q0VU=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/fvbommel/sortorder v1.2.1 h1:ASc85RbthwLEqzuiCk7WQQiopfyGWQiEYZVJ3Qa4A+U=
github.com/fvbommel/sortorder v1.2.1/go.mod h1:LbhO04ijZIeUuvz9B9BkI/qYrpZZEn1gWhxv4QjUKVs=
github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.7 h1:Nac/98uT5LPkfbiFuYa8ktf62a2VxwQ4xbq1GK8pV5E=
github.com/go-jose/go-jose/v4 v4.1.7/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -84,8 +78,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.2 h1:v5tiZvm1KGLyg5d5oXdXe3lsSPrbEUiKoXGia//I7Qo=
github.com/klauspost/compress v1.20.2/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
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/mattn/go-runewidth v0.0.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo=
@@ -94,18 +88,18 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4=
github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc=
github.com/moby/moby/api v1.56.2-0.20261106011722-402d0a16f131 h1:tPz9ahj8F7AjQw62XQFP65kmoxF3rXTYSit3q06/yQc=
github.com/moby/moby/api v1.56.2-0.20261106011722-402d0a16f131/go.mod h1:pRZ1QRtx6AVhdckeFYo4DUX4ASNF0SCllhyY6aj2mgg=
github.com/moby/moby/client v0.6.3-0.20261106011722-402d0a16f131 h1:ReIO7lalQVE4BetZuIvrxScg8Rxv4Sk74XRs06ryRDU=
github.com/moby/moby/client v0.6.3-0.20261106011722-402d0a16f131/go.mod h1:dPyRteJDuLdP8to3kWmNAU/tbMwSFCE88yKHvY/nZ9k=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/swarmkit/v2 v2.1.2 h1:1WDZAI6HVYNKdCG4zlXnTAPyLsLwuhRGWlHoOUf5Z6I=
github.com/moby/swarmkit/v2 v2.1.2/go.mod h1:GQ6T0ij2oBbWX10OHwpvK449xfXkQMZ8J+B+eQ4mgp4=
github.com/moby/swarmkit/v2 v2.1.3 h1:UcwtwkIRjr75qSgUWCTIuzKUzAXlidxsiqVMnyxSF1w=
github.com/moby/swarmkit/v2 v2.1.3/go.mod h1:KhkxT93Hm4wHoNUPJ4JRD384pS7OODFGzJWoDRj9bLE=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/capability v0.5.0 h1:Bm4oD9xGCtZguPnaoQy1/jvKTSwZHMvM0FrnxNYtGYU=
github.com/moby/sys/capability v0.5.0/go.mod h1:i+0cXvNSriGrL3C6sxnmlmGL5HVU0OdxocizXdC8A+g=
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
@@ -161,63 +155,65 @@ 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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY=
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU=
go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4=
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw=
go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 h1:LaLVGJtIzHcvHjVs2OftmKcc+M9mdOqS8vZ1fBD1Yes=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0/go.mod h1:vF6H5sNDuQBJWgSX00iZgbIqk2MZZoVMH0ar5t5bOpw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxOpYPmMNUtOWhdb+nC1WELIgVf8z9higURrbzYU4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc=
go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM=
go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k=
go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v3 v3.0.6 h1:zcDfvjXEqTQmpjniUBLc91xYJMB2c4QOFxEF/LCEckM=
go.yaml.in/yaml/v3 v3.0.6/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/sys v0.49.1-0.20261022125458-f5cb0306ac0e h1:y3PdhEKVIFYc4c6gNZPEC7qPIoa9uH/2zIQEc0mw4R4=
golang.org/x/sys v0.49.1-0.20261022125458-f5cb0306ac0e/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
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.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -230,10 +226,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 h1:izFU9hz7aeLI/Mi1J0991ae+xcwRLr7hTqWnB/9aIIU=
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5/go.mod h1:3LhxRw4YYkf+ylAfgaY9JlVLFKhokkCV8duhLLe7+t0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
@@ -241,7 +237,5 @@ google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
tags.cncf.io/container-device-interface v1.1.1 h1:YPwQz4xg8PlQ0yT/baR0BtLpTQROe4l6M1yuRgAu1vc=
tags.cncf.io/container-device-interface v1.1.1/go.mod h1:S1PSJWYPD4Iom0/39mvr/VVFCfG0Yt14j20d0OYrY2M=
-5
View File
@@ -1,5 +0,0 @@
/.tmp/
*.pprof
*.svg
cover.out
connect.test
-130
View File
@@ -1,130 +0,0 @@
linters-settings:
errcheck:
check-type-assertions: true
exhaustruct:
include:
# No zero values for param structs.
- 'connectrpc\.com/connect\..*[pP]arams'
forbidigo:
forbid:
- '^fmt\.Print'
- '^log\.'
- '^print$'
- '^println$'
- '^panic$'
godox:
# TODO, OPT, etc. comments are fine to commit. Use FIXME comments for
# temporary hacks, and use godox to prevent committing them.
keywords: [FIXME]
importas:
no-unaliased: true
alias:
- pkg: connectrpc.com/connect/internal/gen/connect/ping/v1
alias: pingv1
varnamelen:
ignore-decls:
- T any
- i int
- wg sync.WaitGroup
linters:
enable-all: true
disable:
- cyclop # covered by gocyclo
- depguard # unnecessary for small libraries
- funlen # rely on code review to limit function length
- gocognit # dubious "cognitive overhead" quantification
- gofumpt # prefer standard gofmt
- goimports # rely on gci instead
- inamedparam # convention is not followed
- ireturn # "accept interfaces, return structs" isn't ironclad
- lll # don't want hard limits for line length
- maintidx # covered by gocyclo
- mnd # status codes are clearer than constants
- nlreturn # generous whitespace violates house style
- nonamedreturns # named returns are fine; it's *bare* returns that are bad
- protogetter # too many false positives
- tenv # replaced by usetesting
- testpackage # internal tests are fine
- wrapcheck # don't _always_ need to wrap errors
- wsl # generous whitespace violates house style
issues:
exclude-dirs-use-default: false
exclude:
# Don't ban use of fmt.Errorf to create new errors, but the remaining
# checks from err113 are useful.
- "do not define dynamic errors, use wrapped static errors instead: .*"
exclude-rules:
# If future reflect.Kinds are nil-able, we'll find out when a test fails.
- linters: [exhaustive]
path: internal/assert/assert.go
# We need our duplex HTTP call to have access to the context.
- linters: [containedctx]
path: duplex_http_call.go
# We need to init a global in-mem HTTP server for testable examples.
- linters: [gochecknoinits, gochecknoglobals]
path: example_init_test.go
# We purposefully do an ineffectual assignment for an example.
- linters: [ineffassign]
path: client_example_test.go
# The generated file is effectively a global receiver.
- linters: [varnamelen]
path: cmd/protoc-gen-connect-go
text: "parameter name 'g' is too short"
# Thorough error logging and timeout config make this example unreadably long.
- linters: [errcheck, gosec]
path: error_writer_example_test.go
# It should be crystal clear that Connect uses plain *http.Clients.
- linters: [revive, stylecheck]
path: client_example_test.go
# Don't complain about timeout management or lack of output assertions in examples.
- linters: [gosec, testableexamples]
path: handler_example_test.go
# No output assertions needed for these examples.
- linters: [testableexamples]
path: error_writer_example_test.go
- linters: [testableexamples]
path: error_not_modified_example_test.go
- linters: [testableexamples]
path: error_example_test.go
# In examples, it's okay to use http.ListenAndServe.
- linters: [gosec]
path: error_not_modified_example_test.go
# There are many instances where we want to keep unused parameters
# as a matter of style or convention, for example when a context.Context
# is the first parameter, we choose to just globally ignore this.
- linters: [revive]
text: "^unused-parameter: "
# We want to return explicit nils in protocol_grpc.go
- linters: [revive]
text: "^if-return: "
path: protocol_grpc.go
# We want to return explicit nils in protocol_connect.go
- linters: [revive]
text: "^if-return: "
path: protocol_connect.go
# We want to return explicit nils in error_writer.go
- linters: [revive]
text: "^if-return: "
path: error_writer.go
# We want to set http.Server's logger
- linters: [forbidigo]
path: internal/memhttp
text: "use of `log.(New|Logger|Lshortfile)` forbidden by pattern .*"
# We want to show examples with http.Get
- linters: [noctx]
path: internal/memhttp/memhttp_test.go
# Allow fmt.Sprintf for cmd/protoc-gen-connect-go for consistency
- linters: [perfsprint]
path: cmd/protoc-gen-connect-go/main.go
# Allow non-canonical headers in tests
- linters: [canonicalheader]
path: '.*_test.go'
# Allow Code pointer receiver for UnmarshalText method
- linters: [recvcheck]
path: code.go
# Avoid false positives for int overflow in tests
- linters: [gosec]
text: "^G115: integer overflow conversion"
path: ".*_test.go"
-201
View File
@@ -1,201 +0,0 @@
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 2021-2025 The Connect Authors
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
@@ -1,12 +0,0 @@
Maintainers
===========
## Current
* [Peter Edge](https://github.com/bufdev), [Buf](https://buf.build)
* [Josh Humphries](https://github.com/jhump), [Buf](https://buf.build)
* [Matt Robenolt](https://github.com/mattrobenolt), [PlanetScale](https://planetscale.com)
* [Edward McFarlane](https://github.com/emcfarlane), [Buf](https://buf.build)
## Former
* [Akshay Shah](https://github.com/akshayjshah)
* [Alex McKinney](https://github.com/amckinney)
-121
View File
@@ -1,121 +0,0 @@
# See https://tech.davis-hansson.com/p/make/
SHELL := bash
.DELETE_ON_ERROR:
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := all
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
MAKEFLAGS += --no-print-directory
BIN := .tmp/bin
export PATH := $(abspath $(BIN)):$(PATH)
export GOBIN := $(abspath $(BIN))
COPYRIGHT_YEARS := 2021-2025
LICENSE_IGNORE := --ignore /testdata/
BUF_VERSION := 1.50.1
.PHONY: help
help: ## Describe useful make targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}'
.PHONY: all
all: ## Build, test, and lint (default)
$(MAKE) test
$(MAKE) lint
.PHONY: clean
clean: ## Delete intermediate build artifacts
@# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs
git clean -Xdf
.PHONY: test
test: shorttest slowtest
.PHONY: shorttest
shorttest: build ## Run unit tests
go test -vet=off -race -cover -short ./...
.PHONY: slowtest
# Runs all tests, including known long/slow ones. The
# race detector is not used for a few reasons:
# 1. Race coverage of the short tests should be
# adequate to catch race conditions.
# 2. It slows tests down, which is not good if we
# know these are already slow tests.
# 3. Some of the slow tests can't repro issues and
# find regressions as reliably with the race
# detector enabled.
slowtest: build
go test ./...
.PHONY: runconformance
runconformance: build ## Run conformance test suite
cd internal/conformance && ./runconformance.sh
.PHONY: bench
bench: BENCH ?= .*
bench: build ## Run benchmarks for root package
go test -vet=off -run '^$$' -bench '$(BENCH)' -benchmem -cpuprofile cpu.pprof -memprofile mem.pprof .
.PHONY: build
build: generate ## Build all packages
go build ./...
.PHONY: install
install: ## Install all binaries
go install ./...
.PHONY: lint
lint: $(BIN)/golangci-lint $(BIN)/buf ## Lint Go and protobuf
go vet ./...
golangci-lint run --modules-download-mode=readonly --timeout=3m0s
buf lint
buf format -d --exit-code
.PHONY: lintfix
lintfix: $(BIN)/golangci-lint $(BIN)/buf ## Automatically fix some lint errors
golangci-lint run --fix --modules-download-mode=readonly --timeout=3m0s
buf format -w
.PHONY: generate
generate: $(BIN)/buf $(BIN)/protoc-gen-go $(BIN)/protoc-gen-connect-go $(BIN)/license-header ## Regenerate code and licenses
go mod tidy
cd ./internal/conformance && go mod tidy
buf generate
cd ./cmd/protoc-gen-connect-go/internal && \
find ./testdata -maxdepth 1 -type d \( ! -name testdata \) | xargs -n 1 -I % bash -c "cd '%' && buf generate"
license-header \
--license-type apache \
--copyright-holder "The Connect Authors" \
--year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE)
.PHONY: upgrade
upgrade: ## Upgrade dependencies
go get -u -t ./... && go mod tidy -v
.PHONY: checkgenerate
checkgenerate:
@# Used in CI to verify that `make generate` doesn't produce a diff.
test -z "$$(git status --porcelain | tee /dev/stderr)"
.PHONY: $(BIN)/protoc-gen-connect-go
$(BIN)/protoc-gen-connect-go:
@mkdir -p $(@D)
go build -o $(@) ./cmd/protoc-gen-connect-go
$(BIN)/buf: Makefile
@mkdir -p $(@D)
go install github.com/bufbuild/buf/cmd/buf@v${BUF_VERSION}
$(BIN)/license-header: Makefile
@mkdir -p $(@D)
go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v${BUF_VERSION}
$(BIN)/golangci-lint: Makefile
@mkdir -p $(@D)
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.7
$(BIN)/protoc-gen-go: Makefile go.mod
@mkdir -p $(@D)
@# The version of protoc-gen-go is determined by the version in go.mod
go install google.golang.org/protobuf/cmd/protoc-gen-go
-184
View File
@@ -1,184 +0,0 @@
Connect
=======
[![Build](https://github.com/connectrpc/connect-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/connectrpc/connect-go/actions/workflows/ci.yaml)
[![Report Card](https://goreportcard.com/badge/connectrpc.com/connect)](https://goreportcard.com/report/connectrpc.com/connect)
[![GoDoc](https://pkg.go.dev/badge/connectrpc.com/connect.svg)](https://pkg.go.dev/connectrpc.com/connect)
[![Slack](https://img.shields.io/badge/slack-buf-%23e01563)][slack]
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8972/badge)](https://www.bestpractices.dev/projects/8972)
Connect is a slim library for building browser and gRPC-compatible HTTP APIs.
You write a short [Protocol Buffer][protobuf] schema and implement your
application logic, and Connect generates code to handle marshaling, routing,
compression, and content type negotiation. It also generates an idiomatic,
type-safe client. Handlers and clients support three protocols: gRPC, gRPC-Web,
and Connect's own protocol.
The [Connect protocol][protocol] is a simple protocol that works over HTTP/1.1
or HTTP/2. It takes the best portions of gRPC and gRPC-Web, including
streaming, and packages them into a protocol that works equally well in
browsers, monoliths, and microservices. Calling a Connect API is as easy as
using `curl`. Try it with our live demo:
```
curl \
--header "Content-Type: application/json" \
--data '{"sentence": "I feel happy."}' \
https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say
```
Handlers and clients also support the gRPC and gRPC-Web protocols, including
streaming, headers, trailers, and error details. gRPC-compatible [server
reflection][grpcreflect] and [health checks][grpchealth] are available as
standalone packages. Instead of cURL, we could call our API with a gRPC client:
```
go install github.com/bufbuild/buf/cmd/buf@latest
buf curl --protocol grpc \
--data '{"sentence": "I feel happy."}' \
https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say
```
Under the hood, Connect is just [Protocol Buffers][protobuf] and the standard
library: no custom HTTP implementation, no new name resolution or load
balancing APIs, and no surprises. Everything you already know about `net/http`
still applies, and any package that works with an `http.Server`, `http.Client`,
or `http.Handler` also works with Connect.
For more on Connect, see the [announcement blog post][blog], the documentation
on [connectrpc.com][docs] (especially the [Getting Started] guide for Go), the
[demo service][examples-go], or the [protocol specification][protocol].
## A small example
Curious what all this looks like in practice? From a [Protobuf
schema](internal/proto/connect/ping/v1/ping.proto), we generate [a small RPC
package](internal/gen/simple/connect/ping/v1/pingv1connect/ping.connect.go). Using that
package, we can build a server. This example is available at [internal/example](internal/example):
```go
package main
import (
"context"
"log"
"net/http"
"connectrpc.com/connect"
pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1"
"connectrpc.com/connect/internal/gen/simple/connect/ping/v1/pingv1connect"
"connectrpc.com/validate"
)
type PingServer struct {
pingv1connect.UnimplementedPingServiceHandler // returns errors from all methods
}
func (ps *PingServer) Ping(ctx context.Context, req *pingv1.PingRequest) (*pingv1.PingResponse, error) {
return &pingv1.PingResponse{
Number: req.Number,
}, nil
}
func main() {
mux := http.NewServeMux()
// The generated constructors return a path and a plain net/http
// handler.
mux.Handle(
pingv1connect.NewPingServiceHandler(
&PingServer{},
// Validation via Protovalidate is almost always recommended
connect.WithInterceptors(validate.NewInterceptor()),
),
)
p := new(http.Protocols)
p.SetHTTP1(true)
// For gRPC clients, it's convenient to support HTTP/2 without TLS.
p.SetUnencryptedHTTP2(true)
s := &http.Server{
Addr: "localhost:8080",
Handler: mux,
Protocols: p,
}
if err := s.ListenAndServe(); err != nil {
log.Fatalf("listen failed: %v", err)
}
}
```
With that server running, you can make requests with any gRPC or Connect
client. To write a client using Connect:
```go
package main
import (
"context"
"log"
"net/http"
pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1"
"connectrpc.com/connect/internal/gen/simple/connect/ping/v1/pingv1connect"
)
func main() {
client := pingv1connect.NewPingServiceClient(
http.DefaultClient,
"http://localhost:8080/",
)
req := &pingv1.PingRequest{Number: 42}
res, err := client.Ping(context.Background(), req)
if err != nil {
log.Fatalln(err)
}
log.Println(res)
}
```
Of course, `http.ListenAndServe` and `http.DefaultClient` aren't fit for
production use! See Connect's [deployment docs][docs-deployment] for a guide to
configuring timeouts, connection pools, observability, and h2c.
## Ecosystem
* [grpchealth]: gRPC-compatible health checks for connect-go
* [grpcreflect]: gRPC-compatible server reflection for connect-go
* [validate]: [Protovalidate][protovalidate] interceptor for connect-go
* [examples-go]: service powering [demo.connectrpc.com](https://demo.connectrpc.com), including bidi streaming
* [connect-es]: Type-safe APIs with Protobuf and TypeScript
* [Buf Studio]: web UI for ad-hoc RPCs
* [conformance]: Connect, gRPC, and gRPC-Web interoperability tests
## Status: Stable
This module is stable. It supports:
* The two most recent major releases of Go (the same versions of Go that continue
to [receive security patches][go-support-policy]).
* [APIv2] of Protocol Buffers in Go (`google.golang.org/protobuf`).
Within those parameters, `connect` follows semantic versioning. We will
_not_ make breaking changes in the 1.x series of releases.
## Legal
Offered under the [Apache 2 license][license].
[APIv2]: https://blog.golang.org/protobuf-apiv2
[Buf Studio]: https://buf.build/studio
[Getting Started]: https://connectrpc.com/docs/go/getting-started
[blog]: https://buf.build/blog/connect-a-better-grpc
[conformance]: https://github.com/connectrpc/conformance
[grpchealth]: https://github.com/connectrpc/grpchealth-go
[grpcreflect]: https://github.com/connectrpc/grpcreflect-go
[connect-es]: https://github.com/connectrpc/connect-es
[examples-go]: https://github.com/connectrpc/examples-go
[docs-deployment]: https://connectrpc.com/docs/go/deployment
[docs]: https://connectrpc.com
[go-support-policy]: https://golang.org/doc/devel/release#policy
[license]: https://github.com/connectrpc/connect-go/blob/main/LICENSE
[protobuf]: https://developers.google.com/protocol-buffers
[protocol]: https://connectrpc.com/docs/protocol
[slack]: https://buf.build/links/slack
[validate]: https://github.com/connectrpc/validate-go
[protovalidate]: https://protovalidate.com
-44
View File
@@ -1,44 +0,0 @@
# Releasing connect-go
This document outlines how to create a release of connect-go.
1. Clone the repo, ensuring you have the latest main.
2. On a new branch, open [connect.go](connect.go) and change the `Version` constant to an appropriate [semantic version](https://semver.org/). To select the correct version, look at the version number of the [latest release] and the changes that are included in this new release.
* If there are only bug fixes and no new features, remove the `-dev` suffix, set MINOR number to be equal to the [latest release], and set the PATCH number to be 1 more than the PATCH number of the [latest release].
* If there are features being released, remove the `-dev` suffix, set the MINOR number to be 1 more than the MINOR number of the [latest release], and set the PATCH number to `0`. In the common case, the diff here will just be to remove the `-dev` suffix.
```patch
-const Version = "1.14.0-dev"
+const Version = "1.14.0"
```
3. Check for any changes in [cmd/protoc-gen-connect-go/main.go](cmd/protoc-gen-connect-go/main.go) that require a version restriction. A constant `IsAtLeastVersionX_Y_Z` should be defined in [connect.go](connect.go) if generated code has begun to use a new API. Make sure the generated code references this constant. If a new constant has been added since the last release, ensure that the name of the constant matches the version being released ([Example PR #496](https://github.com/connectrpc/connect-go/pull/496)).
4. Open a PR titled "Prepare for vX.Y.Z" ([Example PR #661](https://github.com/connectrpc/connect-go/pull/661)) and a description tagging all current maintainers. Once it's reviewed and CI passes, merge it.
*Make sure no new commits are merged until the release is complete.*
5. Review all commits in the new release and for each PR check an appropriate label is used and edit the title to be meaninful to end users. This will help auto-generated release notes match the final notes as closely as possible.
6. Using the Github UI, create a new release.
- Under “Choose a tag”, type in “vX.Y.Z” to create a new tag for the release upon publish.
- Target the main branch.
- Title the Release “vX.Y.Z”.
- Click “set as latest release”.
- Set the last version as the “Previous tag”.
- Click “Generate release notes” to autogenerate release notes.
- Edit the release notes. A summary and other sub categories may be added if required but should, in most cases, be left as ### Enhancements and ### Bugfixes. Feel free to collect multiple small changes to docs or Github config into one line, but try to tag every contributor. Make especially sure to credit new external contributors!
7. Publish the release.
8. On a new branch, open [connect.go](connect.go) and change the `Version` to increment the minor tag and append the `-dev` suffix. Use the next minor release - we never anticipate bugs and patch releases.
```patch
-const Version = "1.14.0"
+const Version = "1.15.0-dev"
```
9. Open a PR titled "Back to development" ([Example PR #662](https://github.com/connectrpc/connect-go/pull/662)). Once it's reviewed and CI passes, merge it.
[latest release]: https://github.com/connectrpc/connect-go/releases/latest
-5
View File
@@ -1,5 +0,0 @@
Security Policy
===============
This project follows the [Connect security policy and reporting
process](https://connectrpc.com/docs/governance/security).
-19
View File
@@ -1,19 +0,0 @@
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: connectrpc.com/connect/internal/gen
plugins:
- local: protoc-gen-go
out: internal/gen
opt: paths=source_relative
- local: protoc-gen-connect-go
out: internal/gen/generics
opt: paths=source_relative
- local: protoc-gen-connect-go
out: internal/gen/simple
opt:
- paths=source_relative
- simple
clean: true
-14
View File
@@ -1,14 +0,0 @@
version: v2
modules:
- path: internal/proto
lint:
use:
- STANDARD
ignore:
- internal/proto/connectext/grpc/health/v1/health.proto
- internal/proto/connectext/grpc/reflection/v1alpha/reflection.proto
- internal/proto/connectext/grpc/status/v1/status.proto
disallow_comment_ignores: true
breaking:
use:
- WIRE_JSON
-54
View File
@@ -1,54 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"bytes"
"sync"
)
const (
initialBufferSize = 512
maxRecycleBufferSize = 8 * 1024 * 1024 // if >8MiB, don't hold onto a buffer
)
type bufferPool struct {
sync.Pool
}
func newBufferPool() *bufferPool {
return &bufferPool{
Pool: sync.Pool{
New: func() any {
return bytes.NewBuffer(make([]byte, 0, initialBufferSize))
},
},
}
}
func (b *bufferPool) Get() *bytes.Buffer {
if buf, ok := b.Pool.Get().(*bytes.Buffer); ok {
return buf
}
return bytes.NewBuffer(make([]byte, 0, initialBufferSize))
}
func (b *bufferPool) Put(buffer *bytes.Buffer) {
if buffer.Cap() > maxRecycleBufferSize {
return
}
buffer.Reset()
b.Pool.Put(buffer)
}
-392
View File
@@ -1,392 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// Client is a reusable, concurrency-safe client for a single procedure.
// Depending on the procedure's type, use the CallUnary, CallClientStream,
// CallServerStream, or CallBidiStream method.
//
// By default, clients use the Connect protocol with the binary Protobuf Codec,
// ask for gzipped responses, and send uncompressed requests. To use the gRPC
// or gRPC-Web protocols, use the [WithGRPC] or [WithGRPCWeb] options.
type Client[Req, Res any] struct {
config *clientConfig
callUnary func(context.Context, *Request[Req]) (*Response[Res], error)
protocolClient protocolClient
err error
}
// NewClient constructs a new Client.
func NewClient[Req, Res any](httpClient HTTPClient, url string, options ...ClientOption) *Client[Req, Res] {
client := &Client[Req, Res]{}
config, err := newClientConfig(url, options)
if err != nil {
client.err = err
return client
}
client.config = config
protocolClient, protocolErr := client.config.Protocol.NewClient(
&protocolClientParams{
CompressionName: config.RequestCompressionName,
CompressionPools: newReadOnlyCompressionPools(
config.CompressionPools,
config.CompressionNames,
),
Codec: config.Codec,
Protobuf: config.protobuf(),
CompressMinBytes: config.CompressMinBytes,
HTTPClient: httpClient,
URL: config.URL,
BufferPool: config.BufferPool,
ReadMaxBytes: config.ReadMaxBytes,
SendMaxBytes: config.SendMaxBytes,
EnableGet: config.EnableGet,
GetURLMaxBytes: config.GetURLMaxBytes,
GetUseFallback: config.GetUseFallback,
},
)
if protocolErr != nil {
client.err = protocolErr
return client
}
client.protocolClient = protocolClient
// Rather than applying unary interceptors along the hot path, we can do it
// once at client creation.
unarySpec := config.newSpec(StreamTypeUnary)
unaryFunc := UnaryFunc(func(ctx context.Context, request AnyRequest) (AnyResponse, error) {
conn := client.protocolClient.NewConn(ctx, unarySpec, request.Header())
conn.onRequestSend(func(r *http.Request) {
request.setRequestMethod(r.Method)
callInfo, ok := clientCallInfoForContext(ctx)
if ok {
callInfo.method = r.Method
callInfo.responseSource = conn
}
})
// Send always returns an io.EOF unless the error is from the client-side.
// We want the user to continue to call Receive in those cases to get the
// full error from the server-side.
if err := conn.Send(request.Any()); err != nil && !errors.Is(err, io.EOF) {
_ = conn.CloseRequest()
_ = conn.CloseResponse()
return nil, err
}
if err := conn.CloseRequest(); err != nil {
_ = conn.CloseResponse()
return nil, err
}
response, err := receiveUnaryResponse[Res](conn, config.Initializer)
if err != nil {
_ = conn.CloseResponse()
return nil, err
}
return response, conn.CloseResponse()
})
if interceptor := config.Interceptor; interceptor != nil {
// interceptor is the full chain of all interceptors provided
unaryFunc = interceptor.WrapUnary(unaryFunc)
}
client.callUnary = func(ctx context.Context, request *Request[Req]) (*Response[Res], error) {
// To make the specification, peer, and RPC headers visible to the full
// interceptor chain (as though they were supplied by the caller), we'll
// add them here.
request.spec = unarySpec
request.peer = client.protocolClient.Peer()
protocolClient.WriteRequestHeader(StreamTypeUnary, request.Header())
// Also set them in the context if there's a call info present
callInfo, callInfoOk := clientCallInfoForContext(ctx)
if callInfoOk {
callInfo.peer = request.Peer()
callInfo.spec = request.Spec()
// A client could have set request headers in the call info OR the request wrapper
// So if a callInfo exists in context, merge any headers from there into the request wrapper
// so that all headers are sent in the request
mergeHeaders(request.Header(), callInfo.requestHeader)
// Copy the call info into a sentinel value. This is so we can compare
// the sentinel value against the call info in context. If they're different,
// we can stop the request. This protects against changing the context in interceptors.
ctx = context.WithValue(ctx, sentinelContextKey{}, callInfo)
}
response, err := unaryFunc(ctx, request)
if err != nil {
return nil, err
}
typed, ok := response.(*Response[Res])
if !ok {
return nil, errorf(CodeInternal, "unexpected client response type %T", response)
}
return typed, nil
}
return client
}
// CallUnary calls a request-response procedure.
func (c *Client[Req, Res]) CallUnary(ctx context.Context, request *Request[Req]) (*Response[Res], error) {
if c.err != nil {
return nil, c.err
}
return c.callUnary(ctx, request)
}
// CallClientStream calls a client streaming procedure.
//
// Request headers can be sent via the [ClientStreamForClient.RequestHeader] method on the stream. Note that the
// request headers are not sent automatically when this method is invoked and instead require an explicit call to
// [ClientStreamForClient.Send].
func (c *Client[Req, Res]) CallClientStream(ctx context.Context) *ClientStreamForClient[Req, Res] {
if c.err != nil {
return &ClientStreamForClient[Req, Res]{err: c.err}
}
return &ClientStreamForClient[Req, Res]{
conn: c.newConn(ctx, StreamTypeClient, nil),
initializer: c.config.Initializer,
}
}
// CallClientStreamSimple calls a client streaming procedure.
//
// Request headers should be set in a [CallInfo] object inside the context using [NewClientContext]. These headers are
// transmitted when this method is called and do not require an explicit call to [ClientStreamForClientSimple.Send].
//
// In addition, when calling [ClientStreamForClientSimple.CloseAndReceive] on the returned stream, the returned response
// is the response type defined for the stream and _not_ a Connect [Response] wrapper type. As a result, any response
// headers and trailers should be read from the [CallInfo] object in context.
func (c *Client[Req, Res]) CallClientStreamSimple(ctx context.Context) (*ClientStreamForClientSimple[Req, Res], error) {
if c.err != nil {
return &ClientStreamForClientSimple[Req, Res]{
stream: &ClientStreamForClient[Req, Res]{err: c.err},
}, c.err
}
stream := &ClientStreamForClientSimple[Req, Res]{
stream: &ClientStreamForClient[Req, Res]{
conn: c.newConn(ctx, StreamTypeClient, nil),
initializer: c.config.Initializer,
},
}
if err := stream.Send(nil); err != nil {
return nil, err
}
return stream, nil
}
// CallServerStream calls a server streaming procedure.
func (c *Client[Req, Res]) CallServerStream(ctx context.Context, request *Request[Req]) (*ServerStreamForClient[Res], error) {
if c.err != nil {
return nil, c.err
}
conn := c.newConn(ctx, StreamTypeServer, func(r *http.Request) {
request.method = r.Method
})
request.peer = conn.Peer()
request.spec = conn.Spec()
mergeHeaders(conn.RequestHeader(), request.header)
// Send always returns an io.EOF unless the error is from the client-side.
// We want the user to continue to call Receive in those cases to get the
// full error from the server-side.
if err := conn.Send(request.Msg); err != nil && !errors.Is(err, io.EOF) {
_ = conn.CloseRequest()
_ = conn.CloseResponse()
return nil, err
}
if err := conn.CloseRequest(); err != nil {
return nil, err
}
return &ServerStreamForClient[Res]{
conn: conn,
initializer: c.config.Initializer,
}, nil
}
// CallBidiStream calls a bidirectional streaming procedure.
//
// Request headers can be sent via the [BidiStreamForClient.RequestHeader] method. Note that the
// request headers are not sent automatically when this method is invoked and instead require an explicit call to
// [BidiStreamForClient.Send].
func (c *Client[Req, Res]) CallBidiStream(ctx context.Context) *BidiStreamForClient[Req, Res] {
if c.err != nil {
return &BidiStreamForClient[Req, Res]{err: c.err}
}
return &BidiStreamForClient[Req, Res]{
conn: c.newConn(ctx, StreamTypeBidi, nil),
initializer: c.config.Initializer,
}
}
// CallBidiStreamSimple calls a bidirectional streaming procedure.
//
// Request headers should be set in a [CallInfo] object inside the context using [NewClientContext]. These headers
// are transmitted when this method is called and do not require an explicit call to [BidiStreamForClient.Send].
//
// Likewise, response headers and trailers should be read from the [CallInfo] object in context.
func (c *Client[Req, Res]) CallBidiStreamSimple(ctx context.Context) (*BidiStreamForClientSimple[Req, Res], error) {
if c.err != nil {
return &BidiStreamForClientSimple[Req, Res]{
stream: &BidiStreamForClient[Req, Res]{err: c.err},
}, c.err
}
stream := &BidiStreamForClientSimple[Req, Res]{
stream: &BidiStreamForClient[Req, Res]{
conn: c.newConn(ctx, StreamTypeBidi, nil),
initializer: c.config.Initializer,
},
}
if err := stream.Send(nil); err != nil {
return nil, err
}
return stream, nil
}
func (c *Client[Req, Res]) newConn(ctx context.Context, streamType StreamType, onRequestSend func(r *http.Request)) StreamingClientConn {
callInfo, callInfoOk := clientCallInfoForContext(ctx)
// Set values in the context if there's a call info present
if callInfoOk {
// Copy the call info into a sentinel value. This is so we can compare
// the sentinel value against the call info in context. If they're different,
// we can stop the request. This protects against changing the context in interceptors.
ctx = context.WithValue(ctx, sentinelContextKey{}, callInfo)
}
newConn := func(ctx context.Context, spec Spec) StreamingClientConn {
header := make(http.Header, 8) // arbitrary power of two, prevent immediate resizing
c.protocolClient.WriteRequestHeader(streamType, header)
conn := c.protocolClient.NewConn(ctx, spec, header)
conn.onRequestSend(onRequestSend)
return conn
}
if interceptor := c.config.Interceptor; interceptor != nil {
newConn = interceptor.WrapStreamingClient(newConn)
}
conn := newConn(ctx, c.config.newSpec(streamType))
// Set values in the context if there's a call info present
if callInfoOk {
callInfo.peer = conn.Peer()
callInfo.spec = conn.Spec()
callInfo.responseSource = conn
// Merge any callInfo request headers first, then do the request,
// so that context headers show first in the list of headers.
mergeHeaders(conn.RequestHeader(), callInfo.RequestHeader())
}
return conn
}
type clientConfig struct {
URL *url.URL
Protocol protocol
Procedure string
Schema any
Initializer maybeInitializer
CompressMinBytes int
Interceptor Interceptor
CompressionPools map[string]*compressionPool
CompressionNames []string
Codec Codec
RequestCompressionName string
BufferPool *bufferPool
ReadMaxBytes int
SendMaxBytes int
EnableGet bool
GetURLMaxBytes int
GetUseFallback bool
IdempotencyLevel IdempotencyLevel
}
func newClientConfig(rawURL string, options []ClientOption) (*clientConfig, *Error) {
url, err := parseRequestURL(rawURL)
if err != nil {
return nil, err
}
protoPath := extractProtoPath(url.Path)
config := clientConfig{
URL: url,
Protocol: &protocolConnect{},
Procedure: protoPath,
CompressionPools: make(map[string]*compressionPool),
BufferPool: newBufferPool(),
}
withProtoBinaryCodec().applyToClient(&config)
withGzip().applyToClient(&config)
for _, opt := range options {
opt.applyToClient(&config)
}
if err := config.validate(); err != nil {
return nil, err
}
return &config, nil
}
func (c *clientConfig) validate() *Error {
if c.Codec == nil || c.Codec.Name() == "" {
return errorf(CodeUnknown, "no codec configured")
}
if c.RequestCompressionName != "" && c.RequestCompressionName != compressionIdentity {
if _, ok := c.CompressionPools[c.RequestCompressionName]; !ok {
return errorf(CodeUnknown, "unknown compression %q", c.RequestCompressionName)
}
}
return nil
}
func (c *clientConfig) protobuf() Codec {
if c.Codec.Name() == codecNameProto {
return c.Codec
}
return &protoBinaryCodec{}
}
func (c *clientConfig) newSpec(t StreamType) Spec {
return Spec{
StreamType: t,
Procedure: c.Procedure,
Schema: c.Schema,
IsClient: true,
IdempotencyLevel: c.IdempotencyLevel,
}
}
func parseRequestURL(rawURL string) (*url.URL, *Error) {
url, err := url.ParseRequestURI(rawURL)
if err == nil {
return url, nil
}
if !strings.Contains(rawURL, "://") {
// URL doesn't have a scheme, so the user is likely accustomed to
// grpc-go's APIs.
err = fmt.Errorf(
"URL %q missing scheme: use http:// or https:// (unlike grpc-go)",
rawURL,
)
}
return nil, NewError(CodeUnavailable, err)
}
-441
View File
@@ -1,441 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"errors"
"io"
"net/http"
)
var (
// errNoStreamInitialized signals that a no stream has been initialized when
// attempting to access stream-related methods.
errNoStreamInitialized = errors.New("no stream initialized")
)
// ClientStreamForClient is the client's view of a client streaming RPC.
//
// It's returned from [Client].CallClientStream, but doesn't currently have an
// exported constructor function.
//
// When using this stream, request headers should be set via the [ClientStreamForClient.RequestHeader] method.
type ClientStreamForClient[Req, Res any] struct {
conn StreamingClientConn
initializer maybeInitializer
// Error from client construction. If non-nil, return for all calls.
err error
}
// Spec returns the specification for the RPC.
func (c *ClientStreamForClient[_, _]) Spec() Spec {
return c.conn.Spec()
}
// Peer describes the server for the RPC.
func (c *ClientStreamForClient[_, _]) Peer() Peer {
return c.conn.Peer()
}
// RequestHeader returns the request headers. Headers are sent to the server with the
// first call to Send.
//
// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the
// Connect and gRPC protocols. Applications shouldn't write them.
func (c *ClientStreamForClient[Req, Res]) RequestHeader() http.Header {
if c.err != nil {
return http.Header{}
}
return c.conn.RequestHeader()
}
// Send a message to the server. The first call to Send also sends the request
// headers.
//
// If the server returns an error, Send returns an error that wraps [io.EOF].
// Clients should check for case using the standard library's [errors.Is] and
// unmarshal the error using CloseAndReceive.
func (c *ClientStreamForClient[Req, Res]) Send(request *Req) error {
if c.err != nil {
return c.err
}
if request == nil {
return c.conn.Send(nil)
}
return c.conn.Send(request)
}
// CloseAndReceive closes the send side of the stream and waits for the
// response.
func (c *ClientStreamForClient[Req, Res]) CloseAndReceive() (*Response[Res], error) {
if c.err != nil {
return nil, c.err
}
if err := c.conn.CloseRequest(); err != nil {
_ = c.conn.CloseResponse()
return nil, err
}
response, err := receiveUnaryResponse[Res](c.conn, c.initializer)
if err != nil {
_ = c.conn.CloseResponse()
return nil, err
}
return response, c.conn.CloseResponse()
}
// Conn exposes the underlying StreamingClientConn. This may be useful if
// you'd prefer to wrap the connection in a different high-level API.
func (c *ClientStreamForClient[Req, Res]) Conn() (StreamingClientConn, error) {
return c.conn, c.err
}
// ClientStreamForClientSimple is the client's view of a client streaming RPC.
//
// It's returned from [Client.CallClientStreamSimple], but doesn't currently have an
// exported constructor function.
//
// Usage of this stream requires that request headers be set in a [CallInfo] object in context via [NewClientContext].
// In addition, the response returned by [ClientStreamForClientSimple.CloseAndReceive] is the response type defined for
// the stream and _not_ a Connect [Response] wrapper type. As a result, response headers/trailers should be read from
// the [CallInfo] object in context.
type ClientStreamForClientSimple[Req, Res any] struct {
stream *ClientStreamForClient[Req, Res]
}
// Spec returns the specification for the RPC.
func (c *ClientStreamForClientSimple[_, _]) Spec() Spec {
if c.stream == nil {
return Spec{}
}
return c.stream.Spec()
}
// Peer describes the server for the RPC.
func (c *ClientStreamForClientSimple[_, _]) Peer() Peer {
if c.stream == nil {
return Peer{}
}
return c.stream.Peer()
}
// Send a message to the server. The first call to Send also sends the request
// headers.
//
// If the server returns an error, Send returns an error that wraps [io.EOF].
// Clients should check for case using the standard library's [errors.Is] and
// unmarshal the error using CloseAndReceive.
func (c *ClientStreamForClientSimple[Req, Res]) Send(request *Req) error {
if c.stream == nil {
return errNoStreamInitialized
}
return c.stream.Send(request)
}
// CloseAndReceive closes the send side of the stream and waits for the
// response.
func (c *ClientStreamForClientSimple[Req, Res]) CloseAndReceive() (*Res, error) {
if c.stream == nil {
return nil, errNoStreamInitialized
}
res, err := c.stream.CloseAndReceive()
if err != nil {
return nil, err
}
return res.Msg, nil
}
// ServerStreamForClient is the client's view of a server streaming RPC.
//
// It's returned from [Client].CallServerStream, but doesn't currently have an
// exported constructor function.
type ServerStreamForClient[Res any] struct {
conn StreamingClientConn
initializer maybeInitializer
msg *Res
// Error from client construction. If non-nil, return for all calls.
constructErr error
// Error from conn.Receive().
receiveErr error
}
// Receive advances the stream to the next message, which will then be
// available through the Msg method. It returns false when the stream stops,
// either by reaching the end or by encountering an unexpected error. After
// Receive returns false, the Err method will return any unexpected error
// encountered.
func (s *ServerStreamForClient[Res]) Receive() bool {
if s.constructErr != nil || s.receiveErr != nil {
return false
}
s.msg = new(Res)
if err := s.initializer.maybe(s.conn.Spec(), s.msg); err != nil {
s.receiveErr = err
return false
}
s.receiveErr = s.conn.Receive(s.msg)
return s.receiveErr == nil
}
// Msg returns the most recent message unmarshaled by a call to Receive.
func (s *ServerStreamForClient[Res]) Msg() *Res {
if s.msg == nil {
s.msg = new(Res)
}
return s.msg
}
// Err returns the first non-EOF error that was encountered by Receive.
func (s *ServerStreamForClient[Res]) Err() error {
if s.constructErr != nil {
return s.constructErr
}
if s.receiveErr != nil && !errors.Is(s.receiveErr, io.EOF) {
return s.receiveErr
}
return nil
}
// ResponseHeader returns the headers received from the server. It blocks until
// the first call to Receive returns.
func (s *ServerStreamForClient[Res]) ResponseHeader() http.Header {
if s.constructErr != nil {
return http.Header{}
}
return s.conn.ResponseHeader()
}
// ResponseTrailer returns the trailers received from the server. Trailers
// aren't fully populated until Receive() returns an error wrapping io.EOF.
func (s *ServerStreamForClient[Res]) ResponseTrailer() http.Header {
if s.constructErr != nil {
return http.Header{}
}
return s.conn.ResponseTrailer()
}
// Close the receive side of the stream.
//
// Close is non-blocking. To gracefully close the stream and allow for
// connection resuse ensure all messages have been received before calling
// Close. All messages are received when Receive returns false.
func (s *ServerStreamForClient[Res]) Close() error {
if s.constructErr != nil {
return s.constructErr
}
return s.conn.CloseResponse()
}
// Conn exposes the underlying StreamingClientConn. This may be useful if
// you'd prefer to wrap the connection in a different high-level API.
func (s *ServerStreamForClient[Res]) Conn() (StreamingClientConn, error) {
return s.conn, s.constructErr
}
// BidiStreamForClient is the client's view of a bidirectional streaming RPC.
//
// It's returned from [Client].CallBidiStream, but doesn't currently have an
// exported constructor function.
type BidiStreamForClient[Req, Res any] struct {
conn StreamingClientConn
initializer maybeInitializer
// Error from client construction. If non-nil, return for all calls.
err error
}
// Spec returns the specification for the RPC.
func (b *BidiStreamForClient[_, _]) Spec() Spec {
return b.conn.Spec()
}
// Peer describes the server for the RPC.
func (b *BidiStreamForClient[_, _]) Peer() Peer {
return b.conn.Peer()
}
// RequestHeader returns the request headers. Headers are sent with the first
// call to Send.
//
// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the
// Connect and gRPC protocols. Applications shouldn't write them.
func (b *BidiStreamForClient[Req, Res]) RequestHeader() http.Header {
if b.err != nil {
return http.Header{}
}
return b.conn.RequestHeader()
}
// Send a message to the server. The first call to Send also sends the request
// headers. To send just the request headers, without a body, call Send with a
// nil pointer.
//
// If the server returns an error, Send returns an error that wraps [io.EOF].
// Clients should check for EOF using the standard library's [errors.Is] and
// call Receive to retrieve the error.
func (b *BidiStreamForClient[Req, Res]) Send(msg *Req) error {
if b.err != nil {
return b.err
}
if msg == nil {
return b.conn.Send(nil)
}
return b.conn.Send(msg)
}
// CloseRequest closes the send side of the stream.
func (b *BidiStreamForClient[Req, Res]) CloseRequest() error {
if b.err != nil {
return b.err
}
return b.conn.CloseRequest()
}
// Receive a message. When the server is done sending messages and no other
// errors have occurred, Receive will return an error that wraps [io.EOF].
func (b *BidiStreamForClient[Req, Res]) Receive() (*Res, error) {
if b.err != nil {
return nil, b.err
}
var msg Res
if err := b.initializer.maybe(b.conn.Spec(), &msg); err != nil {
return nil, err
}
if err := b.conn.Receive(&msg); err != nil {
return nil, err
}
return &msg, nil
}
// CloseResponse closes the receive side of the stream.
//
// CloseResponse is non-blocking. To gracefully close the stream and allow for
// connection resuse ensure all messages have been received before calling
// CloseResponse. All messages are received when Receive returns an error
// wrapping [io.EOF].
func (b *BidiStreamForClient[Req, Res]) CloseResponse() error {
if b.err != nil {
return b.err
}
return b.conn.CloseResponse()
}
// ResponseHeader returns the headers received from the server. It blocks until
// the first call to Receive returns.
func (b *BidiStreamForClient[Req, Res]) ResponseHeader() http.Header {
if b.err != nil {
return http.Header{}
}
return b.conn.ResponseHeader()
}
// ResponseTrailer returns the trailers received from the server. Trailers
// aren't fully populated until Receive() returns an error wrapping [io.EOF].
func (b *BidiStreamForClient[Req, Res]) ResponseTrailer() http.Header {
if b.err != nil {
return http.Header{}
}
return b.conn.ResponseTrailer()
}
// Conn exposes the underlying StreamingClientConn. This may be useful if
// you'd prefer to wrap the connection in a different high-level API.
func (b *BidiStreamForClient[Req, Res]) Conn() (StreamingClientConn, error) {
return b.conn, b.err
}
// BidiStreamForClientSimple is the client's view of a bidirectional streaming RPC.
//
// It's returned from [Client].CallBidiStream, but doesn't currently have an
// exported constructor function.
type BidiStreamForClientSimple[Req, Res any] struct {
stream *BidiStreamForClient[Req, Res]
}
// Spec returns the specification for the RPC.
func (b *BidiStreamForClientSimple[_, _]) Spec() Spec {
if b.stream == nil {
return Spec{}
}
return b.stream.Spec()
}
// Peer describes the server for the RPC.
func (b *BidiStreamForClientSimple[_, _]) Peer() Peer {
if b.stream == nil {
return Peer{}
}
return b.stream.Peer()
}
// Send a message to the server. The first call to Send also sends the request
// headers. To send just the request headers, without a body, call Send with a
// nil pointer.
//
// If the server returns an error, Send returns an error that wraps [io.EOF].
// Clients should check for EOF using the standard library's [errors.Is] and
// call Receive to retrieve the error.
func (b *BidiStreamForClientSimple[Req, Res]) Send(msg *Req) error {
if b.stream == nil {
return errNoStreamInitialized
}
return b.stream.Send(msg)
}
// CloseRequest closes the send side of the stream.
func (b *BidiStreamForClientSimple[Req, Res]) CloseRequest() error {
if b.stream == nil {
return errNoStreamInitialized
}
return b.stream.CloseRequest()
}
// Receive a message. When the server is done sending messages and no other
// errors have occurred, Receive will return an error that wraps [io.EOF].
func (b *BidiStreamForClientSimple[Req, Res]) Receive() (*Res, error) {
if b.stream == nil {
return nil, errNoStreamInitialized
}
return b.stream.Receive()
}
// CloseResponse closes the receive side of the stream.
//
// CloseResponse is non-blocking. To gracefully close the stream and allow for
// connection resuse ensure all messages have been received before calling
// CloseResponse. All messages are received when Receive returns an error
// wrapping [io.EOF].
func (b *BidiStreamForClientSimple[Req, Res]) CloseResponse() error {
if b.stream == nil {
return errNoStreamInitialized
}
return b.stream.CloseResponse()
}
// ResponseHeader returns the headers received from the server. It blocks until
// the first call to Receive returns.
func (b *BidiStreamForClientSimple[Req, Res]) ResponseHeader() http.Header {
if b.stream == nil {
return make(http.Header)
}
return b.stream.ResponseHeader()
}
// ResponseTrailer returns the trailers received from the server. Trailers
// aren't fully populated until Receive() returns an error wrapping [io.EOF].
func (b *BidiStreamForClientSimple[Req, Res]) ResponseTrailer() http.Header {
if b.stream == nil {
return make(http.Header)
}
return b.stream.ResponseTrailer()
}
-226
View File
@@ -1,226 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"fmt"
"strconv"
"strings"
)
// A Code is one of the Connect protocol's error codes. There are no user-defined
// codes, so only the codes enumerated below are valid. In both name and
// semantics, these codes match the gRPC status codes.
//
// The descriptions below are optimized for brevity rather than completeness.
// See the [Connect protocol specification] for detailed descriptions of each
// code and example usage.
//
// [Connect protocol specification]: https://connectrpc.com/docs/protocol
type Code uint32
const (
// The zero code in gRPC is OK, which indicates that the operation was a
// success. We don't define a constant for it because it overlaps awkwardly
// with Go's error semantics: what does it mean to have a non-nil error with
// an OK status? (Also, the Connect protocol doesn't use a code for
// successes.)
// CodeCanceled indicates that the operation was canceled, typically by the
// caller.
CodeCanceled Code = 1
// CodeUnknown indicates that the operation failed for an unknown reason.
CodeUnknown Code = 2
// CodeInvalidArgument indicates that client supplied an invalid argument.
CodeInvalidArgument Code = 3
// CodeDeadlineExceeded indicates that deadline expired before the operation
// could complete.
CodeDeadlineExceeded Code = 4
// CodeNotFound indicates that some requested entity (for example, a file or
// directory) was not found.
CodeNotFound Code = 5
// CodeAlreadyExists indicates that client attempted to create an entity (for
// example, a file or directory) that already exists.
CodeAlreadyExists Code = 6
// CodePermissionDenied indicates that the caller doesn't have permission to
// execute the specified operation.
CodePermissionDenied Code = 7
// CodeResourceExhausted indicates that some resource has been exhausted. For
// example, a per-user quota may be exhausted or the entire file system may
// be full.
CodeResourceExhausted Code = 8
// CodeFailedPrecondition indicates that the system is not in a state
// required for the operation's execution.
CodeFailedPrecondition Code = 9
// CodeAborted indicates that operation was aborted by the system, usually
// because of a concurrency issue such as a sequencer check failure or
// transaction abort.
CodeAborted Code = 10
// CodeOutOfRange indicates that the operation was attempted past the valid
// range (for example, seeking past end-of-file).
CodeOutOfRange Code = 11
// CodeUnimplemented indicates that the operation isn't implemented,
// supported, or enabled in this service.
CodeUnimplemented Code = 12
// CodeInternal indicates that some invariants expected by the underlying
// system have been broken. This code is reserved for serious errors.
CodeInternal Code = 13
// CodeUnavailable indicates that the service is currently unavailable. This
// is usually temporary, so clients can back off and retry idempotent
// operations.
CodeUnavailable Code = 14
// CodeDataLoss indicates that the operation has resulted in unrecoverable
// data loss or corruption.
CodeDataLoss Code = 15
// CodeUnauthenticated indicates that the request does not have valid
// authentication credentials for the operation.
CodeUnauthenticated Code = 16
minCode = CodeCanceled
maxCode = CodeUnauthenticated
)
func (c Code) String() string {
switch c {
case CodeCanceled:
return "canceled"
case CodeUnknown:
return "unknown"
case CodeInvalidArgument:
return "invalid_argument"
case CodeDeadlineExceeded:
return "deadline_exceeded"
case CodeNotFound:
return "not_found"
case CodeAlreadyExists:
return "already_exists"
case CodePermissionDenied:
return "permission_denied"
case CodeResourceExhausted:
return "resource_exhausted"
case CodeFailedPrecondition:
return "failed_precondition"
case CodeAborted:
return "aborted"
case CodeOutOfRange:
return "out_of_range"
case CodeUnimplemented:
return "unimplemented"
case CodeInternal:
return "internal"
case CodeUnavailable:
return "unavailable"
case CodeDataLoss:
return "data_loss"
case CodeUnauthenticated:
return "unauthenticated"
}
return fmt.Sprintf("code_%d", c)
}
// MarshalText implements [encoding.TextMarshaler].
func (c Code) MarshalText() ([]byte, error) {
return []byte(c.String()), nil
}
// UnmarshalText implements [encoding.TextUnmarshaler].
func (c *Code) UnmarshalText(data []byte) error {
dataStr := string(data)
switch dataStr {
case "canceled":
*c = CodeCanceled
return nil
case "unknown":
*c = CodeUnknown
return nil
case "invalid_argument":
*c = CodeInvalidArgument
return nil
case "deadline_exceeded":
*c = CodeDeadlineExceeded
return nil
case "not_found":
*c = CodeNotFound
return nil
case "already_exists":
*c = CodeAlreadyExists
return nil
case "permission_denied":
*c = CodePermissionDenied
return nil
case "resource_exhausted":
*c = CodeResourceExhausted
return nil
case "failed_precondition":
*c = CodeFailedPrecondition
return nil
case "aborted":
*c = CodeAborted
return nil
case "out_of_range":
*c = CodeOutOfRange
return nil
case "unimplemented":
*c = CodeUnimplemented
return nil
case "internal":
*c = CodeInternal
return nil
case "unavailable":
*c = CodeUnavailable
return nil
case "data_loss":
*c = CodeDataLoss
return nil
case "unauthenticated":
*c = CodeUnauthenticated
return nil
}
// Ensure that non-canonical codes round-trip through MarshalText and
// UnmarshalText.
if after, ok := strings.CutPrefix(dataStr, "code_"); ok {
dataStr = after
code, err := strconv.ParseUint(dataStr, 10 /* base */, 32 /* bitsize */)
if err == nil && (code < uint64(minCode) || code > uint64(maxCode)) {
*c = Code(code)
return nil
}
}
return fmt.Errorf("invalid code %q", dataStr)
}
// CodeOf returns the error's status code if it is or wraps an [*Error] and
// [CodeUnknown] otherwise.
func CodeOf(err error) Code {
if connectErr, ok := asError(err); ok {
return connectErr.Code()
}
return CodeUnknown
}
-259
View File
@@ -1,259 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/runtime/protoiface"
)
const (
codecNameProto = "proto"
codecNameJSON = "json"
codecNameJSONCharsetUTF8 = codecNameJSON + "; charset=utf-8"
)
// Codec marshals structs (typically generated from a schema) to and from bytes.
type Codec interface {
// Name returns the name of the Codec.
//
// This may be used as part of the Content-Type within HTTP. For example,
// with gRPC this is the content subtype, so "application/grpc+proto" will
// map to the Codec with name "proto".
//
// Names must not be empty.
Name() string
// Marshal marshals the given message.
//
// Marshal may expect a specific type of message, and will error if this type
// is not given.
Marshal(any) ([]byte, error)
// Unmarshal unmarshals the given message.
//
// Unmarshal may expect a specific type of message, and will error if this
// type is not given.
Unmarshal([]byte, any) error
}
// marshalAppender is an extension to Codec for appending to a byte slice.
type marshalAppender interface {
Codec
// MarshalAppend marshals the given message and appends it to the given
// byte slice.
//
// MarshalAppend may expect a specific type of message, and will error if
// this type is not given.
MarshalAppend([]byte, any) ([]byte, error)
}
// stableCodec is an extension to Codec for serializing with stable output.
type stableCodec interface {
Codec
// MarshalStable marshals the given message with stable field ordering.
//
// MarshalStable should return the same output for a given input. Although
// it is not guaranteed to be canonicalized, the marshalling routine for
// MarshalStable will opt for the most normalized output available for a
// given serialization.
//
// For practical reasons, it is possible for MarshalStable to return two
// different results for two inputs considered to be "equal" in their own
// domain, and it may change in the future with codec updates, but for
// any given concrete value and any given version, it should return the
// same output.
MarshalStable(any) ([]byte, error)
// IsBinary returns true if the marshalled data is binary for this codec.
//
// If this function returns false, the data returned from Marshal and
// MarshalStable are considered valid text and may be used in contexts
// where text is expected.
IsBinary() bool
}
type protoBinaryCodec struct{}
var _ Codec = (*protoBinaryCodec)(nil)
func (c *protoBinaryCodec) Name() string { return codecNameProto }
func (c *protoBinaryCodec) Marshal(message any) ([]byte, error) {
protoMessage, ok := message.(proto.Message)
if !ok {
return nil, errNotProto(message)
}
return proto.Marshal(protoMessage)
}
func (c *protoBinaryCodec) MarshalAppend(dst []byte, message any) ([]byte, error) {
protoMessage, ok := message.(proto.Message)
if !ok {
return nil, errNotProto(message)
}
return proto.MarshalOptions{}.MarshalAppend(dst, protoMessage)
}
func (c *protoBinaryCodec) Unmarshal(data []byte, message any) error {
protoMessage, ok := message.(proto.Message)
if !ok {
return errNotProto(message)
}
err := proto.Unmarshal(data, protoMessage)
if err != nil {
return fmt.Errorf("unmarshal into %T: %w", message, err)
}
return nil
}
func (c *protoBinaryCodec) MarshalStable(message any) ([]byte, error) {
protoMessage, ok := message.(proto.Message)
if !ok {
return nil, errNotProto(message)
}
// protobuf does not offer a canonical output today, so this format is not
// guaranteed to match deterministic output from other protobuf libraries.
// In addition, unknown fields may cause inconsistent output for otherwise
// equal messages.
// https://github.com/golang/protobuf/issues/1121
options := proto.MarshalOptions{Deterministic: true}
return options.Marshal(protoMessage)
}
func (c *protoBinaryCodec) IsBinary() bool {
return true
}
type protoJSONCodec struct {
name string
}
var _ Codec = (*protoJSONCodec)(nil)
func (c *protoJSONCodec) Name() string { return c.name }
func (c *protoJSONCodec) Marshal(message any) ([]byte, error) {
protoMessage, ok := message.(proto.Message)
if !ok {
return nil, errNotProto(message)
}
return protojson.MarshalOptions{}.Marshal(protoMessage)
}
func (c *protoJSONCodec) MarshalAppend(dst []byte, message any) ([]byte, error) {
protoMessage, ok := message.(proto.Message)
if !ok {
return nil, errNotProto(message)
}
return protojson.MarshalOptions{}.MarshalAppend(dst, protoMessage)
}
func (c *protoJSONCodec) Unmarshal(binary []byte, message any) error {
protoMessage, ok := message.(proto.Message)
if !ok {
return errNotProto(message)
}
if len(binary) == 0 {
return errors.New("zero-length payload is not a valid JSON object")
}
// Discard unknown fields so clients and servers aren't forced to always use
// exactly the same version of the schema.
options := protojson.UnmarshalOptions{DiscardUnknown: true}
err := options.Unmarshal(binary, protoMessage)
if err != nil {
return fmt.Errorf("unmarshal into %T: %w", message, err)
}
return nil
}
func (c *protoJSONCodec) MarshalStable(message any) ([]byte, error) {
// protojson does not offer a "deterministic" field ordering, but fields
// are still ordered consistently by their index. However, protojson can
// output inconsistent whitespace for some reason, therefore it is
// suggested to use a formatter to ensure consistent formatting.
// https://github.com/golang/protobuf/issues/1373
messageJSON, err := c.Marshal(message)
if err != nil {
return nil, err
}
compactedJSON := bytes.NewBuffer(messageJSON[:0])
if err = json.Compact(compactedJSON, messageJSON); err != nil {
return nil, err
}
return compactedJSON.Bytes(), nil
}
func (c *protoJSONCodec) IsBinary() bool {
return false
}
// readOnlyCodecs is a read-only interface to a map of named codecs.
type readOnlyCodecs interface {
// Get gets the Codec with the given name.
Get(string) Codec
// Protobuf gets the user-supplied protobuf codec, falling back to the default
// implementation if necessary.
//
// This is helpful in the gRPC protocol, where the wire protocol requires
// marshaling protobuf structs to binary even if the RPC procedures were
// generated from a different IDL.
Protobuf() Codec
// Names returns a copy of the registered codec names. The returned slice is
// safe for the caller to mutate.
Names() []string
}
func newReadOnlyCodecs(nameToCodec map[string]Codec) readOnlyCodecs {
return &codecMap{
nameToCodec: nameToCodec,
}
}
type codecMap struct {
nameToCodec map[string]Codec
}
func (m *codecMap) Get(name string) Codec {
return m.nameToCodec[name]
}
func (m *codecMap) Protobuf() Codec {
if pb, ok := m.nameToCodec[codecNameProto]; ok {
return pb
}
return &protoBinaryCodec{}
}
func (m *codecMap) Names() []string {
names := make([]string, 0, len(m.nameToCodec))
for name := range m.nameToCodec {
names = append(names, name)
}
return names
}
func errNotProto(message any) error {
if _, ok := message.(protoiface.MessageV1); ok {
return fmt.Errorf("%T uses github.com/golang/protobuf, but connect-go only supports google.golang.org/protobuf: see https://go.dev/blog/protobuf-apiv2", message)
}
return fmt.Errorf("%T doesn't implement proto.Message", message)
}
-224
View File
@@ -1,224 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"bytes"
"errors"
"io"
"math"
"net/http"
"strings"
"sync"
)
const (
compressionGzip = "gzip"
compressionIdentity = "identity"
)
// A Decompressor is a reusable wrapper that decompresses an underlying data
// source. The standard library's [*gzip.Reader] implements Decompressor.
type Decompressor interface {
io.Reader
// Close closes the Decompressor, but not the underlying data source. It may
// return an error if the Decompressor wasn't read to EOF.
Close() error
// Reset discards the Decompressor's internal state, if any, and prepares it
// to read from a new source of compressed data.
Reset(io.Reader) error
}
// A Compressor is a reusable wrapper that compresses data written to an
// underlying sink. The standard library's [*gzip.Writer] implements Compressor.
type Compressor interface {
io.Writer
// Close flushes any buffered data to the underlying sink, then closes the
// Compressor. It must not close the underlying sink.
Close() error
// Reset discards the Compressor's internal state, if any, and prepares it to
// write compressed data to a new sink.
Reset(io.Writer)
}
type compressionPool struct {
decompressors sync.Pool
compressors sync.Pool
}
func newCompressionPool(
newDecompressor func() Decompressor,
newCompressor func() Compressor,
) *compressionPool {
if newDecompressor == nil && newCompressor == nil {
return nil
}
return &compressionPool{
decompressors: sync.Pool{
New: func() any { return newDecompressor() },
},
compressors: sync.Pool{
New: func() any { return newCompressor() },
},
}
}
func (c *compressionPool) Decompress(dst *bytes.Buffer, src *bytes.Buffer, readMaxBytes int64) *Error {
decompressor, err := c.getDecompressor(src)
if err != nil {
return errorf(CodeInvalidArgument, "get decompressor: %w", err)
}
reader := io.Reader(decompressor)
if readMaxBytes > 0 && readMaxBytes < math.MaxInt64 {
reader = io.LimitReader(decompressor, readMaxBytes+1)
}
bytesRead, err := dst.ReadFrom(reader)
if err != nil {
_ = c.putDecompressor(decompressor)
err = wrapIfContextError(err)
if connectErr, ok := asError(err); ok {
return connectErr
}
return errorf(CodeInvalidArgument, "decompress: %w", err)
}
if readMaxBytes > 0 && bytesRead > readMaxBytes {
discardedBytes, err := io.Copy(io.Discard, decompressor)
_ = c.putDecompressor(decompressor)
if err != nil {
return errorf(CodeResourceExhausted, "message is larger than configured max %d - unable to determine message size: %w", readMaxBytes, err)
}
return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", bytesRead+discardedBytes, readMaxBytes)
}
if err := c.putDecompressor(decompressor); err != nil {
return errorf(CodeUnknown, "recycle decompressor: %w", err)
}
return nil
}
func (c *compressionPool) Compress(dst *bytes.Buffer, src *bytes.Buffer) *Error {
compressor, err := c.getCompressor(dst)
if err != nil {
return errorf(CodeUnknown, "get compressor: %w", err)
}
if _, err := src.WriteTo(compressor); err != nil {
_ = c.putCompressor(compressor)
err = wrapIfContextError(err)
if connectErr, ok := asError(err); ok {
return connectErr
}
return errorf(CodeInternal, "compress: %w", err)
}
if err := c.putCompressor(compressor); err != nil {
return errorf(CodeInternal, "recycle compressor: %w", err)
}
return nil
}
func (c *compressionPool) getDecompressor(reader io.Reader) (Decompressor, error) {
decompressor, ok := c.decompressors.Get().(Decompressor)
if !ok {
return nil, errors.New("expected Decompressor, got incorrect type from pool")
}
return decompressor, decompressor.Reset(reader)
}
func (c *compressionPool) putDecompressor(decompressor Decompressor) error {
if err := decompressor.Close(); err != nil {
return err
}
// While it's in the pool, we don't want the decompressor to retain a
// reference to the underlying reader. However, most decompressors attempt to
// read some header data from the new data source when Reset; since we don't
// know the compression format, we can't provide a valid header. Since we
// also reset the decompressor when it's pulled out of the pool, we can
// ignore errors here.
_ = decompressor.Reset(http.NoBody)
c.decompressors.Put(decompressor)
return nil
}
func (c *compressionPool) getCompressor(writer io.Writer) (Compressor, error) {
compressor, ok := c.compressors.Get().(Compressor)
if !ok {
return nil, errors.New("expected Compressor, got incorrect type from pool")
}
compressor.Reset(writer)
return compressor, nil
}
func (c *compressionPool) putCompressor(compressor Compressor) error {
if err := compressor.Close(); err != nil {
return err
}
compressor.Reset(io.Discard) // don't keep references
c.compressors.Put(compressor)
return nil
}
// readOnlyCompressionPools is a read-only interface to a map of named
// compressionPools.
type readOnlyCompressionPools interface {
Get(string) *compressionPool
Contains(string) bool
// Wordy, but clarifies how this is different from readOnlyCodecs.Names().
CommaSeparatedNames() string
}
func newReadOnlyCompressionPools(
nameToPool map[string]*compressionPool,
reversedNames []string,
) readOnlyCompressionPools {
// Client and handler configs keep compression names in registration order,
// but we want the last registered to be the most preferred.
names := make([]string, 0, len(reversedNames))
seen := make(map[string]struct{}, len(reversedNames))
for i := len(reversedNames) - 1; i >= 0; i-- {
name := reversedNames[i]
if _, ok := seen[name]; ok {
continue
}
seen[name] = struct{}{}
names = append(names, name)
}
return &namedCompressionPools{
nameToPool: nameToPool,
commaSeparatedNames: strings.Join(names, ","),
}
}
type namedCompressionPools struct {
nameToPool map[string]*compressionPool
commaSeparatedNames string
}
func (m *namedCompressionPools) Get(name string) *compressionPool {
if name == "" || name == compressionIdentity {
return nil
}
return m.nameToPool[name]
}
func (m *namedCompressionPools) Contains(name string) bool {
_, ok := m.nameToPool[name]
return ok
}
func (m *namedCompressionPools) CommaSeparatedNames() string {
return m.commaSeparatedNames
}
-482
View File
@@ -1,482 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
// Package connect is a slim RPC framework built on Protocol Buffers and
// [net/http]. In addition to supporting its own protocol, Connect handlers and
// clients are wire-compatible with gRPC and gRPC-Web, including streaming.
//
// This documentation is intended to explain each type and function in
// isolation. Walkthroughs, FAQs, and other narrative docs are available on the
// [Connect website], and there's a working [demonstration service] on Github.
//
// [Connect website]: https://connectrpc.com
// [demonstration service]: https://github.com/connectrpc/examples-go
package connect
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
)
// Version is the semantic version of the connect module.
const Version = "1.19.1"
// These constants are used in compile-time handshakes with connect's generated
// code.
const (
IsAtLeastVersion0_0_1 = true
IsAtLeastVersion0_1_0 = true
IsAtLeastVersion1_7_0 = true
IsAtLeastVersion1_13_0 = true
)
// StreamType describes whether the client, server, neither, or both is
// streaming.
type StreamType uint8
const (
StreamTypeUnary StreamType = 0b00
StreamTypeClient StreamType = 0b01
StreamTypeServer StreamType = 0b10
StreamTypeBidi = StreamTypeClient | StreamTypeServer
)
func (s StreamType) String() string {
switch s {
case StreamTypeUnary:
return "unary"
case StreamTypeClient:
return "client"
case StreamTypeServer:
return "server"
case StreamTypeBidi:
return "bidi"
}
return fmt.Sprintf("stream_%d", s)
}
// StreamingHandlerConn is the server's view of a bidirectional message
// exchange. Interceptors for streaming RPCs may wrap StreamingHandlerConns.
//
// Like the standard library's [http.ResponseWriter], StreamingHandlerConns write
// response headers to the network with the first call to Send. Any subsequent
// mutations are effectively no-ops. Handlers may mutate response trailers at
// any time before returning. When the client has finished sending data,
// Receive returns an error wrapping [io.EOF]. Handlers should check for this
// using the standard library's [errors.Is].
//
// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for
// use by the gRPC and Connect protocols: applications may read them but
// shouldn't write them.
//
// StreamingHandlerConn implementations provided by this module guarantee that
// all returned errors can be cast to [*Error] using the standard library's
// [errors.As].
//
// StreamingHandlerConn implementations do not need to be safe for concurrent use.
type StreamingHandlerConn interface {
Spec() Spec
Peer() Peer
Receive(any) error
RequestHeader() http.Header
Send(any) error
ResponseHeader() http.Header
ResponseTrailer() http.Header
}
// StreamingClientConn is the client's view of a bidirectional message exchange.
// Interceptors for streaming RPCs may wrap StreamingClientConns.
//
// StreamingClientConns write request headers to the network with the first
// call to Send. Any subsequent mutations are effectively no-ops. When the
// server is done sending data, the StreamingClientConn's Receive method
// returns an error wrapping [io.EOF]. Clients should check for this using the
// standard library's [errors.Is]. If the server encounters an error during
// processing, subsequent calls to the StreamingClientConn's Send method will
// return an error wrapping [io.EOF]; clients may then call Receive to unmarshal
// the error.
//
// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for
// use by the gRPC and Connect protocols: applications may read them but
// shouldn't write them.
//
// StreamingClientConn implementations provided by this module guarantee that
// all returned errors can be cast to [*Error] using the standard library's
// [errors.As].
//
// In order to support bidirectional streaming RPCs, all StreamingClientConn
// implementations must support limited concurrent use. See the comments on
// each group of methods for details.
type StreamingClientConn interface {
// Spec and Peer must be safe to call concurrently with all other methods.
Spec() Spec
Peer() Peer
// Send, RequestHeader, and CloseRequest may race with each other, but must
// be safe to call concurrently with all other methods.
Send(any) error
RequestHeader() http.Header
CloseRequest() error
// Receive, ResponseHeader, ResponseTrailer, and CloseResponse may race with
// each other, but must be safe to call concurrently with all other methods.
Receive(any) error
ResponseHeader() http.Header
ResponseTrailer() http.Header
CloseResponse() error
}
// Request is a wrapper around a generated request message. It provides
// access to metadata like headers and the RPC specification, as well as
// strongly-typed access to the message itself.
type Request[T any] struct {
Msg *T
spec Spec
peer Peer
header http.Header
method string
}
// NewRequest wraps a generated request message.
func NewRequest[T any](message *T) *Request[T] {
return &Request[T]{
Msg: message,
// Initialized lazily so we don't allocate unnecessarily.
header: nil,
}
}
// Any returns the concrete request message as an empty interface, so that
// *Request implements the [AnyRequest] interface.
func (r *Request[_]) Any() any {
return r.Msg
}
// Spec returns a description of this RPC.
func (r *Request[_]) Spec() Spec {
return r.spec
}
// Peer describes the other party for this RPC.
func (r *Request[_]) Peer() Peer {
return r.peer
}
// Header returns the HTTP headers for this request. Headers beginning with
// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC
// protocols: applications may read them but shouldn't write them.
func (r *Request[_]) Header() http.Header {
if r.header == nil {
r.header = make(http.Header)
}
return r.header
}
// HTTPMethod returns the HTTP method for this request. This is nearly always
// POST, but side-effect-free unary RPCs could be made via a GET.
//
// On a newly created request, via NewRequest, this will return the empty
// string until the actual request is actually sent and the HTTP method
// determined. This means that client interceptor functions will see the
// empty string until *after* they delegate to the handler they wrapped. It
// is even possible for this to return the empty string after such delegation,
// if the request was never actually sent to the server (and thus no
// determination ever made about the HTTP method).
func (r *Request[_]) HTTPMethod() string {
return r.method
}
// internalOnly implements AnyRequest.
func (r *Request[_]) internalOnly() {}
// setRequestMethod sets the request method to the given value.
func (r *Request[_]) setRequestMethod(method string) {
r.method = method
}
// AnyRequest is the common method set of every [Request], regardless of type
// parameter. It's used in unary interceptors.
//
// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for
// use by the gRPC and Connect protocols: applications may read them but
// shouldn't write them.
//
// To preserve our ability to add methods to this interface without breaking
// backward compatibility, only types defined in this package can implement
// AnyRequest.
type AnyRequest interface {
Any() any
Spec() Spec
Peer() Peer
Header() http.Header
HTTPMethod() string
internalOnly()
setRequestMethod(string)
}
// Response is a wrapper around a generated response message. It provides
// access to metadata like headers and trailers, as well as strongly-typed
// access to the message itself.
type Response[T any] struct {
Msg *T
header http.Header
trailer http.Header
}
// NewResponse wraps a generated response message.
func NewResponse[T any](message *T) *Response[T] {
return &Response[T]{
Msg: message,
// Initialized lazily so we don't allocate unnecessarily.
header: nil,
trailer: nil,
}
}
// Any returns the concrete response message as an empty interface, so that
// *Response implements the [AnyResponse] interface.
func (r *Response[_]) Any() any {
return r.Msg
}
// Header returns the HTTP headers for this response. Headers beginning with
// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC
// protocols: applications may read them but shouldn't write them.
func (r *Response[_]) Header() http.Header {
if r.header == nil {
r.header = make(http.Header)
}
return r.header
}
// Trailer returns the trailers for this response. Depending on the underlying
// RPC protocol, trailers may be sent as HTTP trailers or a protocol-specific
// block of in-body metadata.
//
// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the
// Connect and gRPC protocols: applications may read them but shouldn't write
// them.
func (r *Response[_]) Trailer() http.Header {
if r.trailer == nil {
r.trailer = make(http.Header)
}
return r.trailer
}
// internalOnly implements AnyResponse.
func (r *Response[_]) internalOnly() {}
// AnyResponse is the common method set of every [Response], regardless of type
// parameter. It's used in unary interceptors.
//
// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for
// use by the gRPC and Connect protocols: applications may read them but
// shouldn't write them.
//
// To preserve our ability to add methods to this interface without breaking
// backward compatibility, only types defined in this package can implement
// AnyResponse.
type AnyResponse interface {
Any() any
Header() http.Header
Trailer() http.Header
internalOnly()
}
// HTTPClient is the interface connect expects HTTP clients to implement. The
// standard library's *http.Client implements HTTPClient.
type HTTPClient interface {
Do(*http.Request) (*http.Response, error)
}
// Spec is a description of a client call or a handler invocation.
//
// If you're using Protobuf, protoc-gen-connect-go generates a constant for the
// fully-qualified Procedure corresponding to each RPC in your schema.
type Spec struct {
StreamType StreamType
Schema any // for protobuf RPCs, a protoreflect.MethodDescriptor
Procedure string // for example, "/acme.foo.v1.FooService/Bar"
IsClient bool // otherwise we're in a handler
IdempotencyLevel IdempotencyLevel
}
// Peer describes the other party to an RPC.
//
// When accessed client-side, Addr contains the host or host:port from the
// server's URL. When accessed server-side, Addr contains the client's address
// in IP:port format.
//
// On both the client and the server, Protocol is the RPC protocol in use.
// Currently, it's either [ProtocolConnect], [ProtocolGRPC], or
// [ProtocolGRPCWeb], but additional protocols may be added in the future.
//
// Query contains the query parameters for the request. For the server, this
// will reflect the actual query parameters sent. For the client, it is unset.
type Peer struct {
Addr string
Protocol string
Query url.Values // server-only
}
func newPeerForURL(url *url.URL, protocol string) Peer {
return Peer{
Addr: url.Host,
Protocol: protocol,
}
}
// handlerConnCloser extends StreamingHandlerConn with a method for handlers to
// terminate the message exchange (and optionally send an error to the client).
type handlerConnCloser interface {
StreamingHandlerConn
Close(error) error
}
// receiveConn represents the shared methods of both StreamingClientConn and StreamingHandlerConn
// that the below helper functions use for implementing the rules around a "unary" stream, that
// is expected to have exactly one message (or zero messages followed by a non-EOF error).
type receiveConn interface {
Spec() Spec
Receive(any) error
}
// hasHTTPMethod is implemented by streaming connections that support HTTP methods other than
// POST.
type hasHTTPMethod interface {
getHTTPMethod() string
}
// errStreamingClientConn is a sentinel error implementation of StreamingClientConn.
type errStreamingClientConn struct {
err error
}
func (c *errStreamingClientConn) Receive(msg any) error {
return c.err
}
func (c *errStreamingClientConn) Spec() Spec {
return Spec{}
}
func (c *errStreamingClientConn) Peer() Peer {
return Peer{}
}
func (c *errStreamingClientConn) Send(msg any) error {
return c.err
}
func (c *errStreamingClientConn) CloseRequest() error {
return c.err
}
func (c *errStreamingClientConn) CloseResponse() error {
return c.err
}
func (c *errStreamingClientConn) RequestHeader() http.Header {
return make(http.Header)
}
func (c *errStreamingClientConn) ResponseHeader() http.Header {
return make(http.Header)
}
func (c *errStreamingClientConn) ResponseTrailer() http.Header {
return make(http.Header)
}
// receiveUnaryResponse unmarshals a message from a StreamingClientConn, then
// envelopes the message and attaches headers and trailers. It attempts to
// consume the response stream and isn't appropriate when receiving multiple
// messages.
func receiveUnaryResponse[T any](conn StreamingClientConn, initializer maybeInitializer) (*Response[T], error) {
msg, err := receiveUnaryMessage[T](conn, initializer, "response")
if err != nil {
return nil, err
}
return &Response[T]{
Msg: msg,
header: conn.ResponseHeader(),
trailer: conn.ResponseTrailer(),
}, nil
}
// receiveUnaryRequest unmarshals a message from a StreamingClientConn, then
// envelopes the message and attaches headers and other request properties. It
// attempts to consume the request stream and isn't appropriate when receiving
// multiple messages.
func receiveUnaryRequest[T any](conn StreamingHandlerConn, initializer maybeInitializer) (*Request[T], error) {
msg, err := receiveUnaryMessage[T](conn, initializer, "request")
if err != nil {
return nil, err
}
method := http.MethodPost
if hasRequestMethod, ok := conn.(hasHTTPMethod); ok {
method = hasRequestMethod.getHTTPMethod()
}
return &Request[T]{
Msg: msg,
spec: conn.Spec(),
peer: conn.Peer(),
header: conn.RequestHeader(),
method: method,
}, nil
}
func receiveUnaryMessage[T any](conn receiveConn, initializer maybeInitializer, what string) (*T, error) {
var msg T
if err := initializer.maybe(conn.Spec(), &msg); err != nil {
return nil, err
}
// Possibly counter-intuitive, but the gRPC specs about error codes state that both clients
// and servers should return "unimplemented" when they encounter a cardinality violation: where
// the number of messages in the stream is wrong. Search for "cardinality violation" in the
// following docs:
// https://grpc.github.io/grpc/core/md_doc_statuscodes.html
if err := conn.Receive(&msg); err != nil {
if errors.Is(err, io.EOF) {
err = NewError(CodeUnimplemented, fmt.Errorf("unary %s has zero messages", what))
}
return nil, err
}
// In a well-formed stream, the one message must be the only content in the body.
// To verify that it is well-formed, try to read another message from the stream.
// TODO: optimize this second receive: ideally do it w/out allocation, w/out
// fully reading next message (if one is present), and w/out trying to
// actually unmarshal the bytes)
var msg2 T
if err := initializer.maybe(conn.Spec(), &msg2); err != nil {
return nil, err
}
if err := conn.Receive(&msg2); !errors.Is(err, io.EOF) {
if err == nil {
err = NewError(CodeUnimplemented, fmt.Errorf("unary %s has multiple messages", what))
}
return nil, err
}
return &msg, nil
}
-242
View File
@@ -1,242 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"context"
"net/http"
)
// CallInfo represents information relevant to an RPC call.
type CallInfo interface {
// Spec returns a description of this call.
Spec() Spec
// Peer describes the other party for this call.
Peer() Peer
// RequestHeader returns the HTTP headers for this request. Headers beginning with
// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC
// protocols: applications may read them but shouldn't write them.
RequestHeader() http.Header
// ResponseHeader returns the HTTP headers for this response. Headers beginning with
// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC
// protocols: applications may read them but shouldn't write them.
// On the client side, this method returns nil before
// the call is actually made. After the call is made, for streaming operations,
// this method will block for the server to actually return response headers.
ResponseHeader() http.Header
// ResponseTrailer returns the trailers for this response. Depending on the underlying
// RPC protocol, trailers may be sent as HTTP trailers or a protocol-specific
// block of in-body metadata.
//
// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the
// Connect and gRPC protocols: applications may read them but shouldn't write
// them.
//
// On the client side, this method returns nil before the call is actually made.
// After the call is made, for streaming operations, this method will block
// for the server to actually return response trailers.
ResponseTrailer() http.Header
// HTTPMethod returns the HTTP method for this request. This is nearly always
// POST, but side-effect-free unary RPCs could be made via a GET.
//
// On a newly created request, via NewRequest, this will return the empty
// string until the actual request is actually sent and the HTTP method
// determined. This means that client interceptor functions will see the
// empty string until *after* they delegate to the handler they wrapped. It
// is even possible for this to return the empty string after such delegation,
// if the request was never actually sent to the server (and thus no
// determination ever made about the HTTP method).
HTTPMethod() string
internalOnly()
}
// Create a new client (i.e. outgoing) context for use from a client. When the
// returned context is passed to RPCs, the returned call info can be used to set
// request metadata before the RPC is invoked and to inspect response
// metadata after the RPC completes.
//
// The returned context may be re-used across RPCs as long as they are
// not concurrent. Results of all CallInfo methods other than
// RequestHeader() are undefined if the context is used with concurrent RPCs.
func NewClientContext(ctx context.Context) (context.Context, CallInfo) {
info := &clientCallInfo{}
return context.WithValue(ctx, clientCallInfoContextKey{}, info), info
}
// CallInfoForHandlerContext returns the CallInfo for the given handler (i.e. incoming) context, if there is one.
func CallInfoForHandlerContext(ctx context.Context) (CallInfo, bool) {
value, ok := ctx.Value(handlerCallInfoContextKey{}).(CallInfo)
return value, ok
}
// handlerCallInfo is a CallInfo implementation used for unary handlers.
type handlerCallInfo struct {
spec Spec
peer Peer
method string
requestHeader http.Header
responseHeader http.Header
responseTrailer http.Header
}
func (c *handlerCallInfo) Spec() Spec {
return c.spec
}
func (c *handlerCallInfo) Peer() Peer {
return c.peer
}
func (c *handlerCallInfo) RequestHeader() http.Header {
if c.requestHeader == nil {
c.requestHeader = make(http.Header)
}
return c.requestHeader
}
func (c *handlerCallInfo) ResponseHeader() http.Header {
if c.responseHeader == nil {
c.responseHeader = make(http.Header)
}
return c.responseHeader
}
func (c *handlerCallInfo) ResponseTrailer() http.Header {
if c.responseTrailer == nil {
c.responseTrailer = make(http.Header)
}
return c.responseTrailer
}
func (c *handlerCallInfo) HTTPMethod() string {
return c.method
}
// internalOnly implements CallInfo.
func (c *handlerCallInfo) internalOnly() {}
// streamingHandlerCallInfo is a CallInfo implementation used for streaming RPC handlers.
type streamingHandlerCallInfo struct {
conn StreamingHandlerConn
}
func (c *streamingHandlerCallInfo) Spec() Spec {
return c.conn.Spec()
}
func (c *streamingHandlerCallInfo) Peer() Peer {
return c.conn.Peer()
}
func (c *streamingHandlerCallInfo) RequestHeader() http.Header {
return c.conn.RequestHeader()
}
func (c *streamingHandlerCallInfo) ResponseHeader() http.Header {
return c.conn.ResponseHeader()
}
func (c *streamingHandlerCallInfo) ResponseTrailer() http.Header {
return c.conn.ResponseTrailer()
}
func (c *streamingHandlerCallInfo) HTTPMethod() string {
// All stream calls are POSTs
return http.MethodPost
}
// internalOnly implements CallInfo.
func (c *streamingHandlerCallInfo) internalOnly() {}
// clientCallInfo is a CallInfo implementation used for clients.
type clientCallInfo struct {
responseSource
spec Spec
peer Peer
method string
requestHeader http.Header
}
func (c *clientCallInfo) Spec() Spec {
return c.spec
}
func (c *clientCallInfo) Peer() Peer {
return c.peer
}
func (c *clientCallInfo) RequestHeader() http.Header {
if c.requestHeader == nil {
c.requestHeader = make(http.Header)
}
return c.requestHeader
}
func (c *clientCallInfo) ResponseHeader() http.Header {
if c.responseSource == nil {
return nil
}
return c.responseSource.ResponseHeader()
}
func (c *clientCallInfo) ResponseTrailer() http.Header {
if c.responseSource == nil {
return nil
}
return c.responseSource.ResponseTrailer()
}
func (c *clientCallInfo) HTTPMethod() string {
return c.method
}
// internalOnly implements CallInfo.
func (c *clientCallInfo) internalOnly() {}
// clientCallInfoContextKey is the key used to store client call info in context.
type clientCallInfoContextKey struct{}
// sentinelContextKey is the key used to store a copy of client call info in context
// when a request is made.
// Each step in an interceptor chain compares the actual call info with the
// sentinel call info. If the two values are different, the request will
// return an error in the interceptor.
// This protects against changing the call info in interceptors, which is prohibited
// as it would allow users to modify call info mid-flight independent of the actual
// request or response.
// Users who wish to modify call info data such as headers and trailers should instead
// use Connect [Request] and [Response] wrapper types.
type sentinelContextKey struct{}
// handlerCallInfoContextKey is the key used to store handler call info in context.
type handlerCallInfoContextKey struct{}
// responseSource indicates a type that manages response headers and trailers.
type responseSource interface {
ResponseHeader() http.Header
ResponseTrailer() http.Header
}
// clientCallInfoForContext gets the call info from a client/outgoing context.
func clientCallInfoForContext(ctx context.Context) (*clientCallInfo, bool) {
info, ok := ctx.Value(clientCallInfoContextKey{}).(*clientCallInfo)
return info, ok
}
// newHandlerContext creates a new handler/incoming context.
func newHandlerContext(ctx context.Context, info CallInfo) context.Context {
return context.WithValue(ctx, handlerCallInfoContextKey{}, info)
}
-471
View File
@@ -1,471 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"context"
"errors"
"io"
"net/http"
"net/url"
"sync"
"sync/atomic"
)
// duplexHTTPCall is a full-duplex stream between the client and server. The
// request body is the stream from client to server, and the response body is
// the reverse.
//
// Be warned: we need to use some lesser-known APIs to do this with net/http.
type duplexHTTPCall struct {
ctx context.Context
httpClient HTTPClient
streamType StreamType
onRequestSend func(*http.Request)
validateResponse func(*http.Response) *Error
// io.Pipe is used to implement the request body for client streaming calls.
// If the request is unary, requestBodyWriter is nil.
requestBodyWriter *io.PipeWriter
// requestSent ensures we only send the request once.
requestSent atomic.Bool
request *http.Request
// responseReady is closed when the response is ready or when the request
// fails. Any error on request initialisation will be set on the
// responseErr. There's always a response if responseErr is nil.
responseReady chan struct{}
response *http.Response
responseErr error
}
func newDuplexHTTPCall(
ctx context.Context,
httpClient HTTPClient,
url *url.URL,
spec Spec,
header http.Header,
) *duplexHTTPCall {
// ensure we make a copy of the url before we pass along to the
// Request. This ensures if a transport out of our control wants
// to mutate the req.URL, we don't feel the effects of it.
url = cloneURL(url)
// This is mirroring what http.NewRequestContext did, but
// using an already parsed url.URL object, rather than a string
// and parsing it again. This is a bit funny with HTTP/1.1
// explicitly, but this is logic copied over from
// NewRequestContext and doesn't effect the actual version
// being transmitted.
request := (&http.Request{
Method: http.MethodPost,
URL: url,
Header: header,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Body: http.NoBody,
GetBody: getNoBody,
Host: url.Host,
}).WithContext(ctx)
return &duplexHTTPCall{
ctx: ctx,
httpClient: httpClient,
streamType: spec.StreamType,
request: request,
responseReady: make(chan struct{}),
}
}
// Send sends a message to the server.
func (d *duplexHTTPCall) Send(payload messagePayload) (int64, error) {
if d.streamType&StreamTypeClient == 0 {
return d.sendUnary(payload)
}
isFirst := d.requestSent.CompareAndSwap(false, true)
if isFirst {
// This is the first time we're sending a message to the server.
// We need to send the request headers and start the request.
pipeReader, pipeWriter := io.Pipe()
d.requestBodyWriter = pipeWriter
d.request.Body = pipeReader
d.request.GetBody = nil // GetBody not supported for client streaming
d.request.ContentLength = -1
go d.makeRequest() // concurrent request
}
if err := d.ctx.Err(); err != nil {
return 0, wrapIfContextError(err)
}
if isFirst && payload.Len() == 0 {
// On first write a nil Send is used to send request headers. Avoid
// writing a zero-length payload to avoid superfluous errors with close.
return 0, nil
}
// It's safe to write to this side of the pipe while net/http concurrently
// reads from the other side.
bytesWritten, err := payload.WriteTo(d.requestBodyWriter)
if err != nil && errors.Is(err, io.ErrClosedPipe) {
// Signal that the stream is closed with the more-typical io.EOF instead of
// io.ErrClosedPipe. This makes it easier for protocol-specific wrappers to
// match grpc-go's behavior.
err = io.EOF
}
return bytesWritten, err
}
func (d *duplexHTTPCall) sendUnary(payload messagePayload) (int64, error) {
// Unary messages are sent as a single HTTP request. We don't need to use a
// pipe for the request body and we don't need to send headers separately.
if !d.requestSent.CompareAndSwap(false, true) {
return 0, errors.New("request already sent")
}
payloadLength := int64(payload.Len())
if payloadLength > 0 {
// Build the request body from the payload.
payloadBody := newPayloadCloser(payload)
d.request.Body = payloadBody
d.request.ContentLength = payloadLength
d.request.GetBody = func() (io.ReadCloser, error) {
if !payloadBody.Rewind() {
return nil, errors.New("payload cannot be retried")
}
return payloadBody, nil
}
// Release the payload ensuring that after Send returns the
// payload is safe to be reused. See [http.RoundTripper] for
// more details.
defer payloadBody.Release()
}
d.makeRequest() // synchronous request
if d.responseErr != nil {
// Check on response errors for context errors. Other errors are
// handled on read.
if err := d.ctx.Err(); err != nil {
return 0, wrapIfContextError(err)
}
}
return payloadLength, nil
}
// CloseWrite closes the request body. Callers *must* call CloseWrite before Read when
// using HTTP/1.x.
func (d *duplexHTTPCall) CloseWrite() error {
// Even if Write was never called, we need to make an HTTP request. This
// ensures that we've sent any headers to the server and that we have an HTTP
// response to read from.
if d.requestSent.CompareAndSwap(false, true) {
go d.makeRequest()
// We never setup a request body, so it's effectively already closed.
// So nothing else to do.
return nil
}
// The user calls CloseWrite to indicate that they're done sending data. It's
// safe to close the write side of the pipe while net/http is reading from
// it.
//
// Because connect also supports some RPC types over HTTP/1.1, we need to be
// careful how we expose this method to users. HTTP/1.1 doesn't support
// bidirectional streaming - the write side of the stream (aka request body)
// must be closed before we start reading the response or we'll just block
// forever. To make sure users don't have to worry about this, the generated
// code for unary, client streaming, and server streaming RPCs must call
// CloseWrite automatically rather than requiring the user to do it.
if d.requestBodyWriter != nil {
return d.requestBodyWriter.Close()
}
return d.request.Body.Close()
}
// Header returns the HTTP request headers.
func (d *duplexHTTPCall) Header() http.Header {
return d.request.Header
}
// Trailer returns the HTTP request trailers.
func (d *duplexHTTPCall) Trailer() http.Header {
return d.request.Trailer
}
// URL returns the URL for the request.
func (d *duplexHTTPCall) URL() *url.URL {
return d.request.URL
}
// Method returns the HTTP method for the request (GET or POST).
func (d *duplexHTTPCall) Method() string {
return d.request.Method
}
// SetMethod changes the method of the request before it is sent.
func (d *duplexHTTPCall) SetMethod(method string) {
d.request.Method = method
}
// Read from the response body. Returns the first error passed to SetError.
func (d *duplexHTTPCall) Read(data []byte) (int, error) {
// First, we wait until we've gotten the response headers and established the
// server-to-client side of the stream.
if err := d.BlockUntilResponseReady(); err != nil {
// The stream is already closed or corrupted.
return 0, err
}
// Before we read, check if the context has been canceled.
if err := d.ctx.Err(); err != nil {
return 0, wrapIfContextError(err)
}
n, err := d.response.Body.Read(data)
if err != nil && !errors.Is(err, io.EOF) {
err = wrapIfContextDone(d.ctx, err)
err = wrapIfRSTError(err)
}
return n, err
}
func (d *duplexHTTPCall) CloseRead() error {
_ = d.BlockUntilResponseReady()
if d.response == nil {
return nil
}
err := d.response.Body.Close()
err = wrapIfContextDone(d.ctx, err)
return wrapIfRSTError(err)
}
// ResponseStatusCode is the response's HTTP status code.
func (d *duplexHTTPCall) ResponseStatusCode() (int, error) {
if err := d.BlockUntilResponseReady(); err != nil {
return 0, err
}
return d.response.StatusCode, nil
}
// ResponseHeader returns the response HTTP headers.
func (d *duplexHTTPCall) ResponseHeader() http.Header {
_ = d.BlockUntilResponseReady()
if d.response != nil {
return d.response.Header
}
return make(http.Header)
}
// ResponseTrailer returns the response HTTP trailers.
func (d *duplexHTTPCall) ResponseTrailer() http.Header {
_ = d.BlockUntilResponseReady()
if d.response != nil {
return d.response.Trailer
}
return make(http.Header)
}
// SetValidateResponse sets the response validation function. The function runs
// in a background goroutine.
func (d *duplexHTTPCall) SetValidateResponse(validate func(*http.Response) *Error) {
d.validateResponse = validate
}
// BlockUntilResponseReady returns when the response is ready or reports an
// error from initializing the request.
func (d *duplexHTTPCall) BlockUntilResponseReady() error {
<-d.responseReady
return d.responseErr
}
func (d *duplexHTTPCall) makeRequest() {
// This runs concurrently with Write and CloseWrite. Read and CloseRead wait
// on d.responseReady, so we can't race with them.
defer close(d.responseReady)
// Promote the header Host to the request object.
if host := getHeaderCanonical(d.request.Header, headerHost); len(host) > 0 {
d.request.Host = host
}
if d.onRequestSend != nil {
d.onRequestSend(d.request)
}
// Once we send a message to the server, they send a message back and
// establish the receive side of the stream.
// On error, we close the request body using the Write side of the pipe.
// This ensures HTTP2 streams receive an io.EOF from the Read side of the
// pipe. Write's check for io.ErrClosedPipe and will convert this to io.EOF.
response, err := d.httpClient.Do(d.request) //nolint:bodyclose
if err != nil {
if errors.Is(err, io.EOF) {
// We use io.EOF as a sentinel in many places and don't want this
// transport error to be confused for those other situations.
err = io.ErrUnexpectedEOF
}
err = wrapIfContextError(err)
err = wrapIfLikelyH2CNotConfiguredError(d.request, err)
err = wrapIfLikelyWithGRPCNotUsedError(err)
err = wrapIfRSTError(err)
if _, ok := asError(err); !ok {
err = NewError(CodeUnavailable, err)
}
d.responseErr = err
_ = d.CloseWrite()
return
}
// We've got a response. We can now read from the response body.
// Closing the response body is delegated to the caller even on error.
d.response = response
if err := d.validateResponse(response); err != nil {
d.responseErr = err
_ = d.CloseWrite()
return
}
if (d.streamType&StreamTypeBidi) == StreamTypeBidi && response.ProtoMajor < 2 {
// If we somehow dialed an HTTP/1.x server, fail with an explicit message
// rather than returning a more cryptic error later on.
d.responseErr = errorf(
CodeUnimplemented,
"response from %v is HTTP/%d.%d: bidi streams require at least HTTP/2",
d.request.URL,
response.ProtoMajor,
response.ProtoMinor,
)
_ = d.CloseWrite()
}
}
// getNoBody is a GetBody function for http.NoBody.
func getNoBody() (io.ReadCloser, error) {
return http.NoBody, nil
}
// messagePayload is a sized and seekable message payload. The interface is
// implemented by [*bytes.Reader] and *envelope. Reads must be non-blocking.
type messagePayload interface {
io.Reader
io.WriterTo
io.Seeker
Len() int
}
// nopPayload is a message payload that does nothing. It's used to send headers
// to the server.
type nopPayload struct{}
var _ messagePayload = nopPayload{}
func (nopPayload) Read([]byte) (int, error) {
return 0, io.EOF
}
func (nopPayload) WriteTo(io.Writer) (int64, error) {
return 0, nil
}
func (nopPayload) Seek(int64, int) (int64, error) {
return 0, nil
}
func (nopPayload) Len() int {
return 0
}
// messageSender sends a message payload. The interface is implemented by
// [*duplexHTTPCall] and writeSender.
type messageSender interface {
Send(messagePayload) (int64, error)
}
// writeSender is a sender that writes to an [io.Writer]. Useful for wrapping
// [http.ResponseWriter].
type writeSender struct {
writer io.Writer
}
var _ messageSender = writeSender{}
func (w writeSender) Send(payload messagePayload) (int64, error) {
return payload.WriteTo(w.writer)
}
// See: https://cs.opensource.google/go/go/+/refs/tags/go1.20.1:src/net/http/clone.go;l=22-33
func cloneURL(oldURL *url.URL) *url.URL {
if oldURL == nil {
return nil
}
newURL := new(url.URL)
*newURL = *oldURL
if oldURL.User != nil {
newURL.User = new(url.Userinfo)
*newURL.User = *oldURL.User
}
return newURL
}
// payloadCloser is an [io.ReadCloser] that wraps a messagePayload. It's used to
// implement the request body for unary calls. To safely reuse the buffer
// call Release after the response is received to ensure the payload is safe for
// reuse.
type payloadCloser struct {
mu sync.Mutex
payload messagePayload // nil after Release
}
func newPayloadCloser(payload messagePayload) *payloadCloser {
return &payloadCloser{
payload: payload,
}
}
// Read implements [io.Reader].
func (p *payloadCloser) Read(dst []byte) (readN int, err error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.payload == nil {
return 0, io.EOF
}
return p.payload.Read(dst)
}
// WriteTo implements [io.WriterTo].
func (p *payloadCloser) WriteTo(dst io.Writer) (int64, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.payload == nil {
return 0, nil
}
return p.payload.WriteTo(dst)
}
// Close implements [io.Closer].
func (p *payloadCloser) Close() error {
return nil
}
// Rewind rewinds the payload to the beginning. It returns false if the
// payload has been discarded from a previous call to Release.
func (p *payloadCloser) Rewind() bool {
p.mu.Lock()
defer p.mu.Unlock()
if p.payload == nil {
return false
}
if _, err := p.payload.Seek(0, io.SeekStart); err != nil {
return false
}
return true
}
// Release discards the payload. After Release is called, the payload cannot be
// rewound and the payload is safe to reuse.
func (p *payloadCloser) Release() {
p.mu.Lock()
p.payload = nil
p.mu.Unlock()
}
-387
View File
@@ -1,387 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
)
// flagEnvelopeCompressed indicates that the data is compressed. It has the
// same meaning in the gRPC-Web, gRPC-HTTP2, and Connect protocols.
const flagEnvelopeCompressed = 0b00000001
var errSpecialEnvelope = errorf(
CodeUnknown,
"final message has protocol-specific flags: %w",
// User code checks for end of stream with errors.Is(err, io.EOF).
io.EOF,
)
// envelope is a block of arbitrary bytes wrapped in gRPC and Connect's framing
// protocol.
//
// Each message is preceded by a 5-byte prefix. The first byte is a uint8 used
// as a set of bitwise flags, and the remainder is a uint32 indicating the
// message length. gRPC and Connect interpret the bitwise flags differently, so
// envelope leaves their interpretation up to the caller.
type envelope struct {
Data *bytes.Buffer
Flags uint8
offset int64
}
var _ messagePayload = (*envelope)(nil)
func (e *envelope) IsSet(flag uint8) bool {
return e.Flags&flag == flag
}
// Read implements [io.Reader].
func (e *envelope) Read(data []byte) (readN int, err error) {
if e.offset < 5 {
prefix, err := makeEnvelopePrefix(e.Flags, e.Data.Len())
if err != nil {
return 0, err
}
readN = copy(data, prefix[e.offset:])
e.offset += int64(readN)
if e.offset < 5 {
return readN, nil
}
data = data[readN:]
}
n := copy(data, e.Data.Bytes()[e.offset-5:])
e.offset += int64(n)
readN += n
if readN == 0 && e.offset == int64(e.Data.Len()+5) {
err = io.EOF
}
return readN, err
}
// WriteTo implements [io.WriterTo].
func (e *envelope) WriteTo(dst io.Writer) (wroteN int64, err error) {
if e.offset < 5 {
prefix, err := makeEnvelopePrefix(e.Flags, e.Data.Len())
if err != nil {
return 0, err
}
prefixN, err := dst.Write(prefix[e.offset:])
e.offset += int64(prefixN)
wroteN += int64(prefixN)
if e.offset < 5 {
return wroteN, err
}
}
n, err := dst.Write(e.Data.Bytes()[e.offset-5:])
e.offset += int64(n)
wroteN += int64(n)
return wroteN, err
}
// Seek implements [io.Seeker]. Based on the implementation of [bytes.Reader].
func (e *envelope) Seek(offset int64, whence int) (int64, error) {
var abs int64
switch whence {
case io.SeekStart:
abs = offset
case io.SeekCurrent:
abs = e.offset + offset
case io.SeekEnd:
abs = int64(e.Data.Len()) + offset
default:
return 0, errors.New("connect.envelope.Seek: invalid whence")
}
if abs < 0 {
return 0, errors.New("connect.envelope.Seek: negative position")
}
e.offset = abs
return abs, nil
}
// Len returns the number of bytes of the unread portion of the envelope.
func (e *envelope) Len() int {
if length := int(int64(e.Data.Len()) + 5 - e.offset); length > 0 {
return length
}
return 0
}
type envelopeWriter struct {
ctx context.Context //nolint:containedctx
sender messageSender
codec Codec
compressMinBytes int
compressionPool *compressionPool
bufferPool *bufferPool
sendMaxBytes int
}
func (w *envelopeWriter) Marshal(message any) *Error {
if message == nil {
// Send no-op message to create the request and send headers.
payload := nopPayload{}
if _, err := w.sender.Send(payload); err != nil {
if connectErr, ok := asError(err); ok {
return connectErr
}
return NewError(CodeUnknown, err)
}
return nil
}
if appender, ok := w.codec.(marshalAppender); ok {
return w.marshalAppend(message, appender)
}
return w.marshal(message)
}
// Write writes the enveloped message, compressing as necessary. It doesn't
// retain any references to the supplied envelope or its underlying data.
func (w *envelopeWriter) Write(env *envelope) *Error {
if env.IsSet(flagEnvelopeCompressed) ||
w.compressionPool == nil ||
env.Data.Len() < w.compressMinBytes {
if w.sendMaxBytes > 0 && env.Data.Len() > w.sendMaxBytes {
return errorf(CodeResourceExhausted, "message size %d exceeds sendMaxBytes %d", env.Data.Len(), w.sendMaxBytes)
}
return w.write(env)
}
data := w.bufferPool.Get()
defer w.bufferPool.Put(data)
if err := w.compressionPool.Compress(data, env.Data); err != nil {
return err
}
if w.sendMaxBytes > 0 && data.Len() > w.sendMaxBytes {
return errorf(CodeResourceExhausted, "compressed message size %d exceeds sendMaxBytes %d", data.Len(), w.sendMaxBytes)
}
return w.write(&envelope{
Data: data,
Flags: env.Flags | flagEnvelopeCompressed,
})
}
func (w *envelopeWriter) marshalAppend(message any, codec marshalAppender) *Error {
// Codec supports MarshalAppend; try to re-use a []byte from the pool.
buffer := w.bufferPool.Get()
defer w.bufferPool.Put(buffer)
raw, err := codec.MarshalAppend(buffer.Bytes(), message)
if err != nil {
return errorf(CodeInternal, "marshal message: %w", err)
}
if cap(raw) > buffer.Cap() {
// The buffer from the pool was too small, so MarshalAppend grew the slice.
// Pessimistically assume that the too-small buffer is insufficient for the
// application workload, so there's no point in keeping it in the pool.
// Instead, replace it with the larger, newly-allocated slice. This
// allocates, but it's a small, constant-size allocation.
*buffer = *bytes.NewBuffer(raw)
} else {
// MarshalAppend didn't allocate, but we need to fix the internal state of
// the buffer. Compared to replacing the buffer (as above), buffer.Write
// copies but avoids allocating.
buffer.Write(raw)
}
envelope := &envelope{Data: buffer}
return w.Write(envelope)
}
func (w *envelopeWriter) marshal(message any) *Error {
// Codec doesn't support MarshalAppend; let Marshal allocate a []byte.
raw, err := w.codec.Marshal(message)
if err != nil {
return errorf(CodeInternal, "marshal message: %w", err)
}
buffer := bytes.NewBuffer(raw)
// Put our new []byte into the pool for later reuse.
defer w.bufferPool.Put(buffer)
envelope := &envelope{Data: buffer}
return w.Write(envelope)
}
func (w *envelopeWriter) write(env *envelope) *Error {
if _, err := w.sender.Send(env); err != nil {
err = wrapIfContextDone(w.ctx, err)
if connectErr, ok := asError(err); ok {
return connectErr
}
return errorf(CodeUnknown, "write envelope: %w", err)
}
return nil
}
type envelopeReader struct {
ctx context.Context //nolint:containedctx
reader io.Reader
bytesRead int64 // detect trailers-only gRPC responses
codec Codec
last envelope
compressionPool *compressionPool
bufferPool *bufferPool
readMaxBytes int
}
func (r *envelopeReader) Unmarshal(message any) *Error {
buffer := r.bufferPool.Get()
var dontRelease *bytes.Buffer
defer func() {
if buffer != dontRelease {
r.bufferPool.Put(buffer)
}
}()
env := &envelope{Data: buffer}
err := r.Read(env)
switch {
case err == nil && env.IsSet(flagEnvelopeCompressed) && r.compressionPool == nil:
return errorf(
CodeInternal,
"protocol error: sent compressed message without compression support",
)
case err == nil &&
(env.Flags == 0 || env.Flags == flagEnvelopeCompressed) &&
env.Data.Len() == 0:
// This is a standard message (because none of the top 7 bits are set) and
// there's no data, so the zero value of the message is correct.
return nil
case err != nil && errors.Is(err, io.EOF):
// The stream has ended. Propagate the EOF to the caller.
return err
case err != nil:
// Something's wrong.
return err
}
data := env.Data
if data.Len() > 0 && env.IsSet(flagEnvelopeCompressed) {
decompressed := r.bufferPool.Get()
defer func() {
if decompressed != dontRelease {
r.bufferPool.Put(decompressed)
}
}()
if err := r.compressionPool.Decompress(decompressed, data, int64(r.readMaxBytes)); err != nil {
return err
}
data = decompressed
}
if env.Flags != 0 && env.Flags != flagEnvelopeCompressed {
// Drain the rest of the stream to ensure there is no extra data.
numBytes, err := discard(r.reader)
r.bytesRead += numBytes
if err != nil {
err = wrapIfContextError(err)
if connErr, ok := asError(err); ok {
return connErr
}
return errorf(CodeInternal, "corrupt response: I/O error after end-stream message: %w", err)
} else if numBytes > 0 {
return errorf(CodeInternal, "corrupt response: %d extra bytes after end of stream", numBytes)
}
// One of the protocol-specific flags are set, so this is the end of the
// stream. Save the message for protocol-specific code to process and
// return a sentinel error. We alias the buffer with dontRelease as a
// way of marking it so above defers don't release it to the pool.
r.last = envelope{
Data: data,
Flags: env.Flags,
}
dontRelease = data
return errSpecialEnvelope
}
if err := r.codec.Unmarshal(data.Bytes(), message); err != nil {
return errorf(CodeInvalidArgument, "unmarshal message: %w", err)
}
return nil
}
func (r *envelopeReader) Read(env *envelope) *Error {
prefixes := [5]byte{}
// io.ReadFull reads the number of bytes requested, or returns an error.
// io.EOF will only be returned if no bytes were read.
n, err := io.ReadFull(r.reader, prefixes[:])
r.bytesRead += int64(n)
if err != nil {
if errors.Is(err, io.EOF) {
// The stream ended cleanly. That's expected, but we need to propagate an EOF
// to the user so that they know that the stream has ended. We shouldn't
// add any alarming text about protocol errors, though.
return NewError(CodeUnknown, err)
}
err = wrapIfMaxBytesError(err, "read 5 byte message prefix")
err = wrapIfContextDone(r.ctx, err)
if connectErr, ok := asError(err); ok {
return connectErr
}
// Something else has gone wrong - the stream didn't end cleanly.
return errorf(
CodeInvalidArgument,
"protocol error: incomplete envelope: %w", err,
)
}
size := int64(binary.BigEndian.Uint32(prefixes[1:5]))
if r.readMaxBytes > 0 && size > int64(r.readMaxBytes) {
n, err := io.CopyN(io.Discard, r.reader, size)
r.bytesRead += n
if err != nil && !errors.Is(err, io.EOF) {
return errorf(CodeResourceExhausted, "message is larger than configured max %d - unable to determine message size: %w", r.readMaxBytes, err)
}
return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", size, r.readMaxBytes)
}
// We've read the prefix, so we know how many bytes to expect.
// CopyN will return an error if it doesn't read the requested
// number of bytes.
readN, err := io.CopyN(env.Data, r.reader, size)
r.bytesRead += readN
if err != nil {
if errors.Is(err, io.EOF) {
// We've gotten fewer bytes than we expected, so the stream has ended
// unexpectedly.
return errorf(
CodeInvalidArgument,
"protocol error: promised %d bytes in enveloped message, got %d bytes",
size,
readN,
)
}
err = wrapIfMaxBytesError(err, "read %d byte message", size)
err = wrapIfContextDone(r.ctx, err)
if connectErr, ok := asError(err); ok {
return connectErr
}
return errorf(CodeUnknown, "read enveloped message: %w", err)
}
env.Flags = prefixes[0]
return nil
}
func makeEnvelopePrefix(flags uint8, size int) ([5]byte, error) {
// Cast as 64-bit to ensure comparison works on all architectures.
size64 := int64(size)
if size64 < 0 || size64 > math.MaxUint32 {
return [5]byte{}, fmt.Errorf("connect.makeEnvelopePrefix: size %d out of bounds", size)
}
prefix := [5]byte{}
prefix[0] = flags
binary.BigEndian.PutUint32(prefix[1:5], uint32(size64))
return prefix, nil
}
-458
View File
@@ -1,458 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)
const (
commonErrorsURL = "https://connectrpc.com/docs/go/common-errors"
defaultAnyResolverPrefix = "type.googleapis.com/"
)
var (
// errNotModified signals Connect-protocol responses to GET requests to use the
// 304 Not Modified HTTP error code.
errNotModified = errors.New("not modified")
// errNotModifiedClient wraps ErrNotModified for use client-side.
errNotModifiedClient = fmt.Errorf("HTTP 304: %w", errNotModified)
)
// An ErrorDetail is a self-describing Protobuf message attached to an [*Error].
// Error details are sent over the network to clients, which can then work with
// strongly-typed data rather than trying to parse a complex error message. For
// example, you might use details to send a localized error message or retry
// parameters to the client.
//
// The [google.golang.org/genproto/googleapis/rpc/errdetails] package contains a
// variety of Protobuf messages commonly used as error details.
type ErrorDetail struct {
pbAny *anypb.Any
pbInner proto.Message // if nil, must be extracted from pbAny
wireJSON string // preserve human-readable JSON
}
// NewErrorDetail constructs a new error detail. If msg is an *[anypb.Any] then
// it is used as is. Otherwise, it is first marshalled into an *[anypb.Any]
// value. This returns an error if msg cannot be marshalled.
func NewErrorDetail(msg proto.Message) (*ErrorDetail, error) {
// If it's already an Any, don't wrap it inside another.
if pb, ok := msg.(*anypb.Any); ok {
return &ErrorDetail{pbAny: pb}, nil
}
pb, err := anypb.New(msg)
if err != nil {
return nil, err
}
return &ErrorDetail{pbAny: pb, pbInner: msg}, nil
}
// Type is the fully-qualified name of the detail's Protobuf message (for
// example, acme.foo.v1.FooDetail).
func (d *ErrorDetail) Type() string {
// proto.Any tries to make messages self-describing by using type URLs rather
// than plain type names, but there aren't any descriptor registries
// deployed. With the current state of the `Any` code, it's not possible to
// build a useful type registry either. To hide this from users, we should
// trim the URL prefix is added to the type name.
//
// If we ever want to support remote registries, we can add an explicit
// `TypeURL` method.
return typeNameForURL(d.pbAny.GetTypeUrl())
}
// Bytes returns a copy of the Protobuf-serialized detail.
func (d *ErrorDetail) Bytes() []byte {
out := make([]byte, len(d.pbAny.GetValue()))
copy(out, d.pbAny.GetValue())
return out
}
// Value uses the Protobuf runtime's package-global registry to unmarshal the
// Detail into a strongly-typed message. Typically, clients use Go type
// assertions to cast from the proto.Message interface to concrete types.
func (d *ErrorDetail) Value() (proto.Message, error) {
if d.pbInner != nil {
// We clone it so that if the caller mutates the returned value,
// they don't inadvertently corrupt this error detail value.
return proto.Clone(d.pbInner), nil
}
return d.pbAny.UnmarshalNew()
}
// An Error captures four key pieces of information: a [Code], an underlying Go
// error, a map of metadata, and an optional collection of arbitrary Protobuf
// messages called "details" (more on those below). Servers send the code, the
// underlying error's Error() output, the metadata, and details over the wire
// to clients. Remember that the underlying error's message will be sent to
// clients - take care not to leak sensitive information from public APIs!
//
// Service implementations and interceptors should return errors that can be
// cast to an [*Error] (using the standard library's [errors.As]). If the returned
// error can't be cast to an [*Error], connect will use [CodeUnknown] and the
// returned error's message.
//
// Error details are an optional mechanism for servers, interceptors, and
// proxies to attach arbitrary Protobuf messages to the error code and message.
// They're a clearer and more performant alternative to HTTP header
// microformats. See [the documentation on errors] for more details.
//
// [the documentation on errors]: https://connectrpc.com/docs/go/errors
type Error struct {
code Code
err error
details []*ErrorDetail
meta http.Header
wireErr bool
}
// NewError annotates any Go error with a status code.
func NewError(c Code, underlying error) *Error {
return &Error{code: c, err: underlying}
}
// NewWireError is similar to [NewError], but the resulting *Error returns true
// when tested with [IsWireError].
//
// This is useful for clients trying to propagate partial failures from
// streaming RPCs. Often, these RPCs include error information in their
// response messages (for example, [gRPC server reflection] and
// OpenTelemetry's [OTLP]). Clients propagating these errors up the stack
// should use NewWireError to clarify that the error code, message, and details
// (if any) were explicitly sent by the server rather than inferred from a
// lower-level networking error or timeout.
//
// [gRPC server reflection]: https://github.com/grpc/grpc/blob/v1.49.2/src/proto/grpc/reflection/v1alpha/reflection.proto#L132-L136
// [OTLP]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md#partial-success
func NewWireError(c Code, underlying error) *Error {
err := NewError(c, underlying)
err.wireErr = true
return err
}
// IsWireError checks whether the error was returned by the server, as opposed
// to being synthesized by the client.
//
// Clients may find this useful when deciding how to propagate errors. For
// example, an RPC-to-HTTP proxy might expose a server-sent CodeUnknown as an
// HTTP 500 but a client-synthesized CodeUnknown as a 503.
//
// Handlers will strip [Error.Meta] headers propagated from wire errors to avoid
// leaking response headers. To propagate headers recreate the error as a
// non-wire error.
func IsWireError(err error) bool {
se := new(Error)
if !errors.As(err, &se) {
return false
}
return se.wireErr
}
// NewNotModifiedError indicates that the requested resource hasn't changed. It
// should be used only when handlers wish to respond to conditional HTTP GET
// requests with a 304 Not Modified. In all other circumstances, including all
// RPCs using the gRPC or gRPC-Web protocols, it's equivalent to sending an
// error with [CodeUnknown]. The supplied headers should include Etag,
// Cache-Control, or any other headers required by [RFC 9110 § 15.4.5].
//
// Clients should check for this error using [IsNotModifiedError].
//
// [RFC 9110 § 15.4.5]: https://httpwg.org/specs/rfc9110.html#status.304
func NewNotModifiedError(headers http.Header) *Error {
err := NewError(CodeUnknown, errNotModified)
if headers != nil {
err.meta = headers
}
return err
}
func (e *Error) Error() string {
message := e.Message()
if message == "" {
return e.code.String()
}
return e.code.String() + ": " + message
}
// Message returns the underlying error message. It may be empty if the
// original error was created with a status code and a nil error.
func (e *Error) Message() string {
if e.err != nil {
return e.err.Error()
}
return ""
}
// Unwrap allows [errors.Is] and [errors.As] access to the underlying error.
func (e *Error) Unwrap() error {
return e.err
}
// Code returns the error's status code.
func (e *Error) Code() Code {
return e.code
}
// Details returns the error's details.
func (e *Error) Details() []*ErrorDetail {
return e.details
}
// AddDetail appends to the error's details.
func (e *Error) AddDetail(d *ErrorDetail) {
e.details = append(e.details, d)
}
// Meta allows the error to carry additional information as key-value pairs.
//
// Protocol-specific headers and trailers may be removed to avoid breaking
// protocol semantics. For example, Content-Length and Content-Type headers
// won't be propagated. See the documentation for each protocol for more
// datails.
//
// When clients receive errors, the metadata contains the union of the HTTP
// headers and the protocol-specific trailers (either HTTP trailers or in-body
// metadata).
func (e *Error) Meta() http.Header {
if e.meta == nil {
e.meta = make(http.Header)
}
return e.meta
}
func (e *Error) detailsAsAny() []*anypb.Any {
anys := make([]*anypb.Any, 0, len(e.details))
for _, detail := range e.details {
anys = append(anys, detail.pbAny)
}
return anys
}
// IsNotModifiedError checks whether the supplied error indicates that the
// requested resource hasn't changed. It only returns true if the server used
// [NewNotModifiedError] in response to a Connect-protocol RPC made with an
// HTTP GET.
func IsNotModifiedError(err error) bool {
return errors.Is(err, errNotModified)
}
// errorf calls fmt.Errorf with the supplied template and arguments, then wraps
// the resulting error.
func errorf(c Code, template string, args ...any) *Error {
return NewError(c, fmt.Errorf(template, args...))
}
// asError uses errors.As to unwrap any error and look for a connect *Error.
func asError(err error) (*Error, bool) {
var connectErr *Error
ok := errors.As(err, &connectErr)
return connectErr, ok
}
// wrapIfUncoded ensures that all errors are wrapped. It leaves already-wrapped
// errors unchanged, uses wrapIfContextError to apply codes to context.Canceled
// and context.DeadlineExceeded, and falls back to wrapping other errors with
// CodeUnknown.
func wrapIfUncoded(err error) error {
if err == nil {
return nil
}
maybeCodedErr := wrapIfContextError(err)
if _, ok := asError(maybeCodedErr); ok {
return maybeCodedErr
}
return NewError(CodeUnknown, maybeCodedErr)
}
// wrapIfContextError applies CodeCanceled or CodeDeadlineExceeded to Go's
// context.Canceled and context.DeadlineExceeded errors, but only if they
// haven't already been wrapped.
func wrapIfContextError(err error) error {
if err == nil {
return nil
}
if _, ok := asError(err); ok {
return err
}
if errors.Is(err, context.Canceled) {
return NewError(CodeCanceled, err)
}
if errors.Is(err, context.DeadlineExceeded) {
return NewError(CodeDeadlineExceeded, err)
}
// Ick, some dial errors can be returned as os.ErrDeadlineExceeded
// instead of context.DeadlineExceeded :(
// https://github.com/golang/go/issues/64449
if errors.Is(err, os.ErrDeadlineExceeded) {
return NewError(CodeDeadlineExceeded, err)
}
return err
}
// wrapIfContextDone wraps errors with CodeCanceled or CodeDeadlineExceeded
// if the context is done. It leaves already-wrapped errors unchanged.
func wrapIfContextDone(ctx context.Context, err error) error {
if err == nil {
return nil
}
err = wrapIfContextError(err)
if _, ok := asError(err); ok {
return err
}
ctxErr := ctx.Err()
if errors.Is(ctxErr, context.Canceled) {
return NewError(CodeCanceled, err)
} else if errors.Is(ctxErr, context.DeadlineExceeded) {
return NewError(CodeDeadlineExceeded, err)
}
return err
}
// wrapIfLikelyH2CNotConfiguredError adds a wrapping error that has a message
// telling the caller that they likely need to use h2c but are using a raw http.Client{}.
//
// This happens when running a gRPC-only server.
// This is fragile and may break over time, and this should be considered a best-effort.
func wrapIfLikelyH2CNotConfiguredError(request *http.Request, err error) error {
if err == nil {
return nil
}
if _, ok := asError(err); ok {
return err
}
if url := request.URL; url != nil && url.Scheme != "http" {
// If the scheme is not http, we definitely do not have an h2c error, so just return.
return err
}
// net/http code has been investigated and there is no typing of any of these errors
// they are all created with fmt.Errorf
// grpc-go returns the first error 2/3-3/4 of the time, and the second error 1/4-1/3 of the time
if errString := err.Error(); strings.HasPrefix(errString, `Post "`) &&
(strings.Contains(errString, `net/http: HTTP/1.x transport connection broken: malformed HTTP response`) ||
strings.HasSuffix(errString, `write: broken pipe`)) {
return fmt.Errorf("possible h2c configuration issue when talking to gRPC server, see %s: %w", commonErrorsURL, err)
}
return err
}
// wrapIfLikelyWithGRPCNotUsedError adds a wrapping error that has a message
// telling the caller that they likely forgot to use connect.WithGRPC().
//
// This happens when running a gRPC-only server.
// This is fragile and may break over time, and this should be considered a best-effort.
func wrapIfLikelyWithGRPCNotUsedError(err error) error {
if err == nil {
return nil
}
if _, ok := asError(err); ok {
return err
}
// golang.org/x/net code has been investigated and there is no typing of this error
// it is created with fmt.Errorf
// http2/transport.go:573: return nil, fmt.Errorf("http2: Transport: cannot retry err [%v] after Request.Body was written; define Request.GetBody to avoid this error", err)
if errString := err.Error(); strings.HasPrefix(errString, `Post "`) &&
strings.Contains(errString, `http2: Transport: cannot retry err`) &&
strings.HasSuffix(errString, `after Request.Body was written; define Request.GetBody to avoid this error`) {
return fmt.Errorf("possible missing connect.WithGPRC() client option when talking to gRPC server, see %s: %w", commonErrorsURL, err)
}
return err
}
// HTTP/2 has its own set of error codes, which it sends in RST_STREAM frames.
// When the server sends one of these errors, we should map it back into our
// RPC error codes following
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#http2-transport-mapping.
//
// This would be vastly simpler if we were using x/net/http2 directly, since
// the StreamError type is exported. When x/net/http2 gets vendored into
// net/http, though, all these types become unexported...so we're left with
// string munging.
func wrapIfRSTError(err error) error {
const (
streamErrPrefix = "stream error: "
fromPeerSuffix = "; received from peer"
)
if err == nil {
return nil
}
if _, ok := asError(err); ok {
return err
}
if urlErr := new(url.Error); errors.As(err, &urlErr) {
// If we get an RST_STREAM error from http.Client.Do, it's wrapped in a
// *url.Error.
err = urlErr.Unwrap()
}
msg := err.Error()
if !strings.HasPrefix(msg, streamErrPrefix) {
return err
}
if !strings.HasSuffix(msg, fromPeerSuffix) {
return err
}
msg = strings.TrimSuffix(msg, fromPeerSuffix)
i := strings.LastIndex(msg, ";")
if i < 0 || i >= len(msg)-1 {
return err
}
msg = msg[i+1:]
msg = strings.TrimSpace(msg)
switch msg {
case "NO_ERROR", "PROTOCOL_ERROR", "INTERNAL_ERROR", "FLOW_CONTROL_ERROR",
"SETTINGS_TIMEOUT", "FRAME_SIZE_ERROR", "COMPRESSION_ERROR", "CONNECT_ERROR":
return NewError(CodeInternal, err)
case "REFUSED_STREAM":
return NewError(CodeUnavailable, err)
case "CANCEL":
return NewError(CodeCanceled, err)
case "ENHANCE_YOUR_CALM":
return NewError(CodeResourceExhausted, fmt.Errorf("bandwidth exhausted: %w", err))
case "INADEQUATE_SECURITY":
return NewError(CodePermissionDenied, fmt.Errorf("transport protocol insecure: %w", err))
default:
return err
}
}
// wrapIfMaxBytesError wraps errors returned reading from a http.MaxBytesHandler
// whose limit has been exceeded.
func wrapIfMaxBytesError(err error, tmpl string, args ...any) error {
if err == nil {
return nil
}
if _, ok := asError(err); ok {
return err
}
var maxBytesErr *http.MaxBytesError
if ok := errors.As(err, &maxBytesErr); !ok {
return err
}
prefix := fmt.Sprintf(tmpl, args...)
return errorf(CodeResourceExhausted, "%s: exceeded %d byte http.MaxBytesReader limit", prefix, maxBytesErr.Limit)
}
func typeNameForURL(url string) string {
return url[strings.LastIndexByte(url, '/')+1:]
}
-179
View File
@@ -1,179 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
// protocolType is one of the supported RPC protocols.
type protocolType uint8
const (
unknownProtocol protocolType = iota
connectUnaryProtocol
connectStreamProtocol
grpcProtocol
grpcWebProtocol
)
// An ErrorWriter writes errors to an [http.ResponseWriter] in the format
// expected by an RPC client. This is especially useful in server-side net/http
// middleware, where you may wish to handle requests from RPC and non-RPC
// clients with the same code.
//
// ErrorWriters are safe to use concurrently.
type ErrorWriter struct {
bufferPool *bufferPool
protobuf Codec
requireConnectProtocolHeader bool
}
// NewErrorWriter constructs an ErrorWriter. Handler options may be passed to
// configure the error writer behaviour to match the handlers.
// [WithRequireConnectProtocolHeader] will assert that Connect protocol
// requests include the version header allowing the error writer to correctly
// classify the request.
// Options supplied via [WithConditionalHandlerOptions] are ignored.
func NewErrorWriter(opts ...HandlerOption) *ErrorWriter {
config := newHandlerConfig("", StreamTypeUnary, opts)
codecs := newReadOnlyCodecs(config.Codecs)
return &ErrorWriter{
bufferPool: config.BufferPool,
protobuf: codecs.Protobuf(),
requireConnectProtocolHeader: config.RequireConnectProtocolHeader,
}
}
func (w *ErrorWriter) classifyRequest(request *http.Request) protocolType {
ctype := canonicalizeContentType(getHeaderCanonical(request.Header, headerContentType))
isPost := request.Method == http.MethodPost
isGet := request.Method == http.MethodGet
switch {
case isPost && (ctype == grpcContentTypeDefault || strings.HasPrefix(ctype, grpcContentTypePrefix)):
return grpcProtocol
case isPost && (ctype == grpcWebContentTypeDefault || strings.HasPrefix(ctype, grpcWebContentTypePrefix)):
return grpcWebProtocol
case isPost && strings.HasPrefix(ctype, connectStreamingContentTypePrefix):
// Streaming ignores the requireConnectProtocolHeader option as the
// Content-Type is enough to determine the protocol.
if err := connectCheckProtocolVersion(request, false /* required */); err != nil {
return unknownProtocol
}
return connectStreamProtocol
case isPost && strings.HasPrefix(ctype, connectUnaryContentTypePrefix):
if err := connectCheckProtocolVersion(request, w.requireConnectProtocolHeader); err != nil {
return unknownProtocol
}
return connectUnaryProtocol
case isGet:
if err := connectCheckProtocolVersion(request, w.requireConnectProtocolHeader); err != nil {
return unknownProtocol
}
return connectUnaryProtocol
default:
return unknownProtocol
}
}
// IsSupported checks whether a request is using one of the ErrorWriter's
// supported RPC protocols.
func (w *ErrorWriter) IsSupported(request *http.Request) bool {
return w.classifyRequest(request) != unknownProtocol
}
// Write an error, using the format appropriate for the RPC protocol in use.
// Callers should first use IsSupported to verify that the request is using one
// of the ErrorWriter's supported RPC protocols. If the protocol is unknown,
// Write will send the error as unprefixed, Connect-formatted JSON.
//
// Write does not read or close the request body.
func (w *ErrorWriter) Write(response http.ResponseWriter, request *http.Request, err error) error {
ctype := canonicalizeContentType(getHeaderCanonical(request.Header, headerContentType))
switch protocolType := w.classifyRequest(request); protocolType {
case connectStreamProtocol:
setHeaderCanonical(response.Header(), headerContentType, ctype)
return w.writeConnectStreaming(response, err)
case grpcProtocol:
setHeaderCanonical(response.Header(), headerContentType, ctype)
return w.writeGRPC(response, err)
case grpcWebProtocol:
setHeaderCanonical(response.Header(), headerContentType, ctype)
return w.writeGRPCWeb(response, err)
case unknownProtocol, connectUnaryProtocol:
fallthrough
default:
// Unary errors are always JSON. Unknown protocols are treated as unary
// because they are likely to be Connect clients and will still be able to
// parse the error as it's in a human-readable format.
setHeaderCanonical(response.Header(), headerContentType, connectUnaryContentTypeJSON)
return w.writeConnectUnary(response, err)
}
}
func (w *ErrorWriter) writeConnectUnary(response http.ResponseWriter, err error) error {
if connectErr, ok := asError(err); ok && !connectErr.wireErr {
mergeNonProtocolHeaders(response.Header(), connectErr.meta)
}
response.WriteHeader(connectCodeToHTTP(CodeOf(err)))
data, marshalErr := json.Marshal(newConnectWireError(err))
if marshalErr != nil {
return fmt.Errorf("marshal error: %w", marshalErr)
}
_, writeErr := response.Write(data)
return writeErr
}
func (w *ErrorWriter) writeConnectStreaming(response http.ResponseWriter, err error) error {
response.WriteHeader(http.StatusOK)
marshaler := &connectStreamingMarshaler{
envelopeWriter: envelopeWriter{
sender: writeSender{writer: response},
bufferPool: w.bufferPool,
},
}
// MarshalEndStream returns *Error: check return value to avoid typed nils.
if marshalErr := marshaler.MarshalEndStream(err, make(http.Header)); marshalErr != nil {
return marshalErr
}
return nil
}
func (w *ErrorWriter) writeGRPC(response http.ResponseWriter, err error) error {
trailers := make(http.Header, 2) // need space for at least code & message
grpcErrorToTrailer(trailers, w.protobuf, err)
// To make net/http reliably send trailers without a body, we must set the
// Trailers header rather than using http.TrailerPrefix. See
// https://github.com/golang/go/issues/54723.
keys := make([]string, 0, len(trailers))
for k := range trailers {
keys = append(keys, k)
}
setHeaderCanonical(response.Header(), headerTrailer, strings.Join(keys, ","))
response.WriteHeader(http.StatusOK)
mergeHeaders(response.Header(), trailers)
return nil
}
func (w *ErrorWriter) writeGRPCWeb(response http.ResponseWriter, err error) error {
// This is a trailers-only response. To match the behavior of Envoy and
// protocol_grpc.go, put the trailers in the HTTP headers.
grpcErrorToTrailer(response.Header(), w.protobuf, err)
response.WriteHeader(http.StatusOK)
return nil
}
-423
View File
@@ -1,423 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"context"
"net/http"
)
// A Handler is the server-side implementation of a single RPC defined by a
// service schema.
//
// By default, Handlers support the Connect, gRPC, and gRPC-Web protocols with
// the binary Protobuf and JSON codecs. They support gzip compression using the
// standard library's [compress/gzip].
type Handler struct {
spec Spec
implementation StreamingHandlerFunc
protocolHandlers map[string][]protocolHandler // Method to protocol handlers
allowMethod string // Allow header
acceptPost string // Accept-Post header
}
// NewUnaryHandler constructs a [Handler] for a request-response procedure.
func NewUnaryHandler[Req, Res any](
procedure string,
unary func(context.Context, *Request[Req]) (*Response[Res], error),
options ...HandlerOption,
) *Handler {
// Wrap the strongly-typed implementation so we can apply interceptors.
untyped := UnaryFunc(func(ctx context.Context, request AnyRequest) (AnyResponse, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
typed, ok := request.(*Request[Req])
if !ok {
return nil, errorf(CodeInternal, "unexpected handler request type %T", request)
}
res, err := unary(ctx, typed)
if res == nil && err == nil {
// This is going to panic during serialization. Debugging is much easier
// if we panic here instead, so we can include the procedure name.
panic(procedure + " returned nil *connect.Response and nil error") //nolint: forbidigo
}
return res, err
})
config := newHandlerConfig(procedure, StreamTypeUnary, options)
if interceptor := config.Interceptor; interceptor != nil {
untyped = interceptor.WrapUnary(untyped)
}
// Given a stream, how should we call the unary function?
implementation := func(ctx context.Context, conn StreamingHandlerConn) error {
request, err := receiveUnaryRequest[Req](conn, config.Initializer)
if err != nil {
return err
}
// Add the request header to the context, and store the response header
// and trailer to propagate back to the caller.
info := &handlerCallInfo{
peer: request.Peer(),
spec: request.Spec(),
method: request.HTTPMethod(),
requestHeader: request.Header(),
}
ctx = newHandlerContext(ctx, info)
response, err := untyped(ctx, request)
// Add response headers/trailers from the context callinfo into the conn if they exist
if info.responseHeader != nil {
mergeNonProtocolHeaders(conn.ResponseHeader(), info.responseHeader)
}
if info.responseTrailer != nil {
mergeNonProtocolHeaders(conn.ResponseTrailer(), info.responseTrailer)
}
if err != nil {
return err
}
// Add response headers/trailers from the response into the conn if they exist
if len(response.Header()) != 0 {
mergeNonProtocolHeaders(conn.ResponseHeader(), response.Header())
}
if len(response.Trailer()) != 0 {
mergeNonProtocolHeaders(conn.ResponseTrailer(), response.Trailer())
}
return conn.Send(response.Any())
}
protocolHandlers := config.newProtocolHandlers()
return &Handler{
spec: config.newSpec(),
implementation: implementation,
protocolHandlers: mappedMethodHandlers(protocolHandlers),
allowMethod: sortedAllowMethodValue(protocolHandlers),
acceptPost: sortedAcceptPostValue(protocolHandlers),
}
}
// NewUnaryHandlerSimple constructs a [Handler] for a request-response procedure using the
// function signature associated with the "simple" generation option.
//
// This option eliminates the [Request] and [Response] wrappers, and instead uses the
// context.Context to propagate information such as headers.
func NewUnaryHandlerSimple[Req, Res any](
procedure string,
unary func(context.Context, *Req) (*Res, error),
options ...HandlerOption,
) *Handler {
return NewUnaryHandler(
procedure,
func(ctx context.Context, request *Request[Req]) (*Response[Res], error) {
responseMsg, err := unary(ctx, request.Msg)
if err != nil {
return nil, err
}
return NewResponse(responseMsg), nil
},
options...,
)
}
// NewClientStreamHandler constructs a [Handler] for a client streaming procedure.
func NewClientStreamHandler[Req, Res any](
procedure string,
implementation func(context.Context, *ClientStream[Req]) (*Response[Res], error),
options ...HandlerOption,
) *Handler {
config := newHandlerConfig(procedure, StreamTypeClient, options)
return newStreamHandler(
config,
func(ctx context.Context, conn StreamingHandlerConn) error {
stream := &ClientStream[Req]{
conn: conn,
initializer: config.Initializer,
}
ctx = newHandlerContext(ctx, &streamingHandlerCallInfo{
conn: conn,
})
res, err := implementation(ctx, stream)
if err != nil {
return err
}
if res == nil {
// This is going to panic during serialization. Debugging is much easier
// if we panic here instead, so we can include the procedure name.
panic(procedure + " returned nil *connect.Response and nil error") //nolint: forbidigo
}
mergeHeaders(conn.ResponseHeader(), res.header)
mergeHeaders(conn.ResponseTrailer(), res.trailer)
return conn.Send(res.Msg)
},
)
}
// NewClientStreamHandlerSimple constructs a [Handler] for a request-streaming procedure
// using the function signature associated with the "simple" generation option.
//
// This option eliminates the [Response] wrapper, and instead uses the context.Context
// to propagate information such as headers.
func NewClientStreamHandlerSimple[Req, Res any](
procedure string,
implementation func(context.Context, *ClientStream[Req]) (*Res, error),
options ...HandlerOption,
) *Handler {
return NewClientStreamHandler(
procedure,
func(ctx context.Context, stream *ClientStream[Req]) (*Response[Res], error) {
responseMsg, err := implementation(ctx, stream)
if err != nil {
return nil, err
}
return NewResponse(responseMsg), nil
},
options...,
)
}
// NewServerStreamHandler constructs a [Handler] for a server streaming procedure.
func NewServerStreamHandler[Req, Res any](
procedure string,
implementation func(context.Context, *Request[Req], *ServerStream[Res]) error,
options ...HandlerOption,
) *Handler {
config := newHandlerConfig(procedure, StreamTypeServer, options)
return newStreamHandler(
config,
func(ctx context.Context, conn StreamingHandlerConn) error {
req, err := receiveUnaryRequest[Req](conn, config.Initializer)
if err != nil {
return err
}
ctx = newHandlerContext(ctx, &streamingHandlerCallInfo{
conn: conn,
})
return implementation(ctx, req, &ServerStream[Res]{conn: conn})
},
)
}
// NewServerStreamHandlerSimple constructs a [Handler] a server streaming procedure using the function
// signature associated with the "simple" generation option.
//
// This option eliminates the [Request] wrapper, and instead uses the context.Context to
// propagate information such as headers.
func NewServerStreamHandlerSimple[Req, Res any](
procedure string,
implementation func(context.Context, *Req, *ServerStream[Res]) error,
options ...HandlerOption,
) *Handler {
return NewServerStreamHandler(
procedure,
func(ctx context.Context, request *Request[Req], serverStream *ServerStream[Res]) error {
return implementation(ctx, request.Msg, serverStream)
},
options...,
)
}
// NewBidiStreamHandler constructs a [Handler] for a bidirectional streaming procedure.
func NewBidiStreamHandler[Req, Res any](
procedure string,
implementation func(context.Context, *BidiStream[Req, Res]) error,
options ...HandlerOption,
) *Handler {
config := newHandlerConfig(procedure, StreamTypeBidi, options)
return newStreamHandler(
config,
func(ctx context.Context, conn StreamingHandlerConn) error {
ctx = newHandlerContext(ctx, &streamingHandlerCallInfo{
conn: conn,
})
return implementation(
ctx,
&BidiStream[Req, Res]{
conn: conn,
initializer: config.Initializer,
},
)
},
)
}
// ServeHTTP implements [http.Handler].
func (h *Handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
// We don't need to defer functions to close the request body or read to
// EOF: the stream we construct later on already does that, and we only
// return early when dealing with misbehaving clients. In those cases, it's
// okay if we can't re-use the connection.
isBidi := (h.spec.StreamType & StreamTypeBidi) == StreamTypeBidi
if isBidi && request.ProtoMajor < 2 {
// Clients coded to expect full-duplex connections may hang if they've
// mistakenly negotiated HTTP/1.1. To unblock them, we must close the
// underlying TCP connection.
responseWriter.Header().Set("Connection", "close")
responseWriter.WriteHeader(http.StatusHTTPVersionNotSupported)
return
}
protocolHandlers := h.protocolHandlers[request.Method]
if len(protocolHandlers) == 0 {
responseWriter.Header().Set("Allow", h.allowMethod)
responseWriter.WriteHeader(http.StatusMethodNotAllowed)
return
}
contentType := canonicalizeContentType(getHeaderCanonical(request.Header, headerContentType))
// Find our implementation of the RPC protocol in use.
var protocolHandler protocolHandler
for _, handler := range protocolHandlers {
if handler.CanHandlePayload(request, contentType) {
protocolHandler = handler
break
}
}
if protocolHandler == nil {
responseWriter.Header().Set("Accept-Post", h.acceptPost)
responseWriter.WriteHeader(http.StatusUnsupportedMediaType)
return
}
if request.Method == http.MethodGet {
// A body must not be present.
hasBody := request.ContentLength > 0
if request.ContentLength < 0 {
// No content-length header.
// Test if body is empty by trying to read a single byte.
var b [1]byte
n, _ := request.Body.Read(b[:])
hasBody = n > 0
}
if hasBody {
responseWriter.WriteHeader(http.StatusUnsupportedMediaType)
return
}
_ = request.Body.Close()
}
// Establish a stream and serve the RPC.
setHeaderCanonical(request.Header, headerContentType, contentType)
setHeaderCanonical(request.Header, headerHost, request.Host)
ctx, cancel, timeoutErr := protocolHandler.SetTimeout(request) //nolint: contextcheck
if timeoutErr != nil {
ctx = request.Context()
}
if cancel != nil {
defer cancel()
}
connCloser, ok := protocolHandler.NewConn(
responseWriter,
request.WithContext(ctx),
)
if !ok {
// Failed to create stream, usually because client used an unknown
// compression algorithm. Nothing further to do.
return
}
if timeoutErr != nil {
_ = connCloser.Close(timeoutErr)
return
}
_ = connCloser.Close(h.implementation(ctx, connCloser))
}
type handlerConfig struct {
CompressionPools map[string]*compressionPool
CompressionNames []string
Codecs map[string]Codec
CompressMinBytes int
Interceptor Interceptor
Procedure string
Schema any
Initializer maybeInitializer
RequireConnectProtocolHeader bool
IdempotencyLevel IdempotencyLevel
BufferPool *bufferPool
ReadMaxBytes int
SendMaxBytes int
StreamType StreamType
}
func newHandlerConfig(procedure string, streamType StreamType, options []HandlerOption) *handlerConfig {
protoPath := extractProtoPath(procedure)
config := handlerConfig{
Procedure: protoPath,
CompressionPools: make(map[string]*compressionPool),
Codecs: make(map[string]Codec),
BufferPool: newBufferPool(),
StreamType: streamType,
}
withProtoBinaryCodec().applyToHandler(&config)
withProtoJSONCodecs().applyToHandler(&config)
withGzip().applyToHandler(&config)
for _, opt := range options {
opt.applyToHandler(&config)
}
return &config
}
func (c *handlerConfig) newSpec() Spec {
return Spec{
Procedure: c.Procedure,
Schema: c.Schema,
StreamType: c.StreamType,
IdempotencyLevel: c.IdempotencyLevel,
}
}
func (c *handlerConfig) newProtocolHandlers() []protocolHandler {
protocols := []protocol{
&protocolConnect{},
&protocolGRPC{web: false},
&protocolGRPC{web: true},
}
handlers := make([]protocolHandler, 0, len(protocols))
codecs := newReadOnlyCodecs(c.Codecs)
compressors := newReadOnlyCompressionPools(
c.CompressionPools,
c.CompressionNames,
)
for _, protocol := range protocols {
handlers = append(handlers, protocol.NewHandler(&protocolHandlerParams{
Spec: c.newSpec(),
Codecs: codecs,
CompressionPools: compressors,
CompressMinBytes: c.CompressMinBytes,
BufferPool: c.BufferPool,
ReadMaxBytes: c.ReadMaxBytes,
SendMaxBytes: c.SendMaxBytes,
RequireConnectProtocolHeader: c.RequireConnectProtocolHeader,
IdempotencyLevel: c.IdempotencyLevel,
}))
}
return handlers
}
func newStreamHandler(
config *handlerConfig,
implementation StreamingHandlerFunc,
) *Handler {
if ic := config.Interceptor; ic != nil {
implementation = ic.WrapStreamingHandler(implementation)
}
protocolHandlers := config.newProtocolHandlers()
return &Handler{
spec: config.newSpec(),
implementation: implementation,
protocolHandlers: mappedMethodHandlers(protocolHandlers),
allowMethod: sortedAllowMethodValue(protocolHandlers),
acceptPost: sortedAcceptPostValue(protocolHandlers),
}
}
-198
View File
@@ -1,198 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"errors"
"io"
"net/http"
)
// ClientStream is the handler's view of a client streaming RPC.
//
// It's constructed as part of [Handler] invocation, but doesn't currently have
// an exported constructor.
type ClientStream[Req any] struct {
conn StreamingHandlerConn
initializer maybeInitializer
msg *Req
err error
}
// Spec returns the specification for the RPC.
func (c *ClientStream[_]) Spec() Spec {
return c.conn.Spec()
}
// Peer describes the client for this RPC.
func (c *ClientStream[_]) Peer() Peer {
return c.conn.Peer()
}
// RequestHeader returns the headers received from the client.
func (c *ClientStream[Req]) RequestHeader() http.Header {
return c.conn.RequestHeader()
}
// Receive advances the stream to the next message, which will then be
// available through the Msg method. It returns false when the stream stops,
// either by reaching the end or by encountering an unexpected error. After
// Receive returns false, the Err method will return any unexpected error
// encountered.
func (c *ClientStream[Req]) Receive() bool {
if c.err != nil {
return false
}
c.msg = new(Req)
if err := c.initializer.maybe(c.Spec(), c.msg); err != nil {
c.err = err
return false
}
c.err = c.conn.Receive(c.msg)
return c.err == nil
}
// Msg returns the most recent message unmarshaled by a call to Receive.
func (c *ClientStream[Req]) Msg() *Req {
if c.msg == nil {
c.msg = new(Req)
}
return c.msg
}
// Err returns the first non-EOF error that was encountered by Receive.
func (c *ClientStream[Req]) Err() error {
if c.err == nil || errors.Is(c.err, io.EOF) {
return nil
}
return c.err
}
// Conn exposes the underlying StreamingHandlerConn. This may be useful if
// you'd prefer to wrap the connection in a different high-level API.
func (c *ClientStream[Req]) Conn() StreamingHandlerConn {
return c.conn
}
// ServerStream is the handler's view of a server streaming RPC.
//
// It's constructed as part of [Handler] invocation, but doesn't currently have
// an exported constructor.
type ServerStream[Res any] struct {
conn StreamingHandlerConn
}
// ResponseHeader returns the response headers. Headers are sent with the first
// call to Send.
//
// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the
// Connect and gRPC protocols. Applications shouldn't write them.
func (s *ServerStream[Res]) ResponseHeader() http.Header {
return s.conn.ResponseHeader()
}
// ResponseTrailer returns the response trailers. Handlers may write to the
// response trailers at any time before returning.
//
// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the
// Connect and gRPC protocols. Applications shouldn't write them.
func (s *ServerStream[Res]) ResponseTrailer() http.Header {
return s.conn.ResponseTrailer()
}
// Send a message to the client. The first call to Send also sends the response
// headers.
func (s *ServerStream[Res]) Send(msg *Res) error {
if msg == nil {
return s.conn.Send(nil)
}
return s.conn.Send(msg)
}
// Conn exposes the underlying StreamingHandlerConn. This may be useful if
// you'd prefer to wrap the connection in a different high-level API.
func (s *ServerStream[Res]) Conn() StreamingHandlerConn {
return s.conn
}
// BidiStream is the handler's view of a bidirectional streaming RPC.
//
// It's constructed as part of [Handler] invocation, but doesn't currently have
// an exported constructor.
type BidiStream[Req, Res any] struct {
conn StreamingHandlerConn
initializer maybeInitializer
}
// Spec returns the specification for the RPC.
func (b *BidiStream[_, _]) Spec() Spec {
return b.conn.Spec()
}
// Peer describes the client for this RPC.
func (b *BidiStream[_, _]) Peer() Peer {
return b.conn.Peer()
}
// RequestHeader returns the headers received from the client.
func (b *BidiStream[Req, Res]) RequestHeader() http.Header {
return b.conn.RequestHeader()
}
// Receive a message. When the client is done sending messages, Receive will
// return an error that wraps [io.EOF].
func (b *BidiStream[Req, Res]) Receive() (*Req, error) {
var req Req
if err := b.initializer.maybe(b.Spec(), &req); err != nil {
return nil, err
}
if err := b.conn.Receive(&req); err != nil {
return nil, err
}
return &req, nil
}
// ResponseHeader returns the response headers. Headers are sent with the first
// call to Send.
//
// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the
// Connect and gRPC protocols. Applications shouldn't write them.
func (b *BidiStream[Req, Res]) ResponseHeader() http.Header {
return b.conn.ResponseHeader()
}
// ResponseTrailer returns the response trailers. Handlers may write to the
// response trailers at any time before returning.
//
// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the
// Connect and gRPC protocols. Applications shouldn't write them.
func (b *BidiStream[Req, Res]) ResponseTrailer() http.Header {
return b.conn.ResponseTrailer()
}
// Send a message to the client. The first call to Send also sends the response
// headers.
func (b *BidiStream[Req, Res]) Send(msg *Res) error {
if msg == nil {
return b.conn.Send(nil)
}
return b.conn.Send(msg)
}
// Conn exposes the underlying StreamingHandlerConn. This may be useful if
// you'd prefer to wrap the connection in a different high-level API.
func (b *BidiStream[Req, Res]) Conn() StreamingHandlerConn {
return b.conn
}
-128
View File
@@ -1,128 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"encoding/base64"
"net/http"
)
//nolint:gochecknoglobals
var protocolHeaders = map[string]struct{}{
// HTTP headers.
headerContentType: {},
headerContentLength: {},
headerContentEncoding: {},
headerHost: {},
headerUserAgent: {},
headerTrailer: {},
headerDate: {},
// Connect headers.
connectUnaryHeaderAcceptCompression: {},
connectUnaryTrailerPrefix: {},
connectStreamingHeaderCompression: {},
connectStreamingHeaderAcceptCompression: {},
connectHeaderTimeout: {},
connectHeaderProtocolVersion: {},
// gRPC headers.
grpcHeaderCompression: {},
grpcHeaderAcceptCompression: {},
grpcHeaderTimeout: {},
grpcHeaderStatus: {},
grpcHeaderMessage: {},
grpcHeaderDetails: {},
}
// EncodeBinaryHeader base64-encodes the data. It always emits unpadded values.
//
// In the Connect, gRPC, and gRPC-Web protocols, binary headers must have keys
// ending in "-Bin".
func EncodeBinaryHeader(data []byte) string {
// gRPC specification says that implementations should emit unpadded values.
return base64.RawStdEncoding.EncodeToString(data)
}
// DecodeBinaryHeader base64-decodes the data. It can decode padded or unpadded
// values. Following usual HTTP semantics, multiple base64-encoded values may
// be joined with a comma. When receiving such comma-separated values, split
// them with [strings.Split] before calling DecodeBinaryHeader.
//
// Binary headers sent using the Connect, gRPC, and gRPC-Web protocols have
// keys ending in "-Bin".
func DecodeBinaryHeader(data string) ([]byte, error) {
if len(data)%4 != 0 {
// Data definitely isn't padded.
return base64.RawStdEncoding.DecodeString(data)
}
// Either the data was padded, or padding wasn't necessary. In both cases,
// the padding-aware decoder works.
return base64.StdEncoding.DecodeString(data)
}
func mergeHeaders(into, from http.Header) {
for key, vals := range from {
if len(vals) == 0 {
// For response trailers, net/http will pre-populate entries
// with nil values based on the "Trailer" header. But if there
// are no actual values for those keys, we skip them.
continue
}
into[key] = append(into[key], vals...)
}
}
// mergeNonProtocolHeaders merges headers excluding protocol headers defined in
// protocolHeaders.
func mergeNonProtocolHeaders(into, from http.Header) {
for key, vals := range from {
if len(vals) == 0 {
// For response trailers, net/http will pre-populate entries
// with nil values based on the "Trailer" header. But if there
// are no actual values for those keys, we skip them.
continue
}
if _, isProtocolHeader := protocolHeaders[key]; !isProtocolHeader {
into[key] = append(into[key], vals...)
}
}
}
// getHeaderCanonical is a shortcut for Header.Get() which
// bypasses the CanonicalMIMEHeaderKey operation when we
// know the key is already in canonical form.
func getHeaderCanonical(h http.Header, key string) string {
if h == nil {
return ""
}
v := h[key]
if len(v) == 0 {
return ""
}
return v[0]
}
// setHeaderCanonical is a shortcut for Header.Set() which
// bypasses the CanonicalMIMEHeaderKey operation when we
// know the key is already in canonical form.
func setHeaderCanonical(h http.Header, key, value string) {
h[key] = []string{value}
}
// delHeaderCanonical is a shortcut for Header.Del() which
// bypasses the CanonicalMIMEHeaderKey operation when we
// know the key is already in canonical form.
func delHeaderCanonical(h http.Header, key string) {
delete(h, key)
}
-68
View File
@@ -1,68 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import "fmt"
// An IdempotencyLevel is a value that declares how "idempotent" an RPC is. This
// value can affect RPC behaviors, such as determining whether it is safe to
// retry a request, or what kinds of request modalities are allowed for a given
// procedure.
type IdempotencyLevel int
// NOTE: For simplicity, these should be kept in sync with the values of the
// google.protobuf.MethodOptions.IdempotencyLevel enumeration.
const (
// IdempotencyUnknown is the default idempotency level. A procedure with
// this idempotency level may not be idempotent. This is appropriate for
// any kind of procedure.
IdempotencyUnknown IdempotencyLevel = 0
// IdempotencyNoSideEffects is the idempotency level that specifies that a
// given call has no side-effects. This is equivalent to [RFC 9110 § 9.2.1]
// "safe" methods in terms of semantics. This procedure should not mutate
// any state. This idempotency level is appropriate for queries, or anything
// that would be suitable for an HTTP GET request. In addition, due to the
// lack of side-effects, such a procedure would be suitable to retry and
// expect that the results will not be altered by preceding attempts.
//
// [RFC 9110 § 9.2.1]: https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.1
IdempotencyNoSideEffects IdempotencyLevel = 1
// IdempotencyIdempotent is the idempotency level that specifies that a
// given call is "idempotent", such that multiple instances of the same
// request to this procedure would have the same side-effects as a single
// request. This is equivalent to [RFC 9110 § 9.2.2] "idempotent" methods.
// This level is a subset of the previous level. This idempotency level is
// appropriate for any procedure that is safe to retry multiple times
// and be guaranteed that the response and side-effects will not be altered
// as a result of multiple attempts, for example, entity deletion requests.
//
// [RFC 9110 § 9.2.2]: https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2
IdempotencyIdempotent IdempotencyLevel = 2
)
func (i IdempotencyLevel) String() string {
switch i {
case IdempotencyUnknown:
return "idempotency_unknown"
case IdempotencyNoSideEffects:
return "no_side_effects"
case IdempotencyIdempotent:
return "idempotent"
}
return fmt.Sprintf("idempotency_%d", i)
}
-138
View File
@@ -1,138 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"context"
"errors"
)
var (
// errNewClientContextProhibited signals that a new client context was created
// in an interceptor, which is prohibited.
errNewClientContextProhibited = errors.New("creating a new context in an interceptor is prohibited")
)
// UnaryFunc is the generic signature of a unary RPC. Interceptors may wrap
// Funcs.
//
// The type of the request and response structs depend on the codec being used.
// When using Protobuf, request.Any() and response.Any() will always be
// [proto.Message] implementations.
type UnaryFunc func(context.Context, AnyRequest) (AnyResponse, error)
// StreamingClientFunc is the generic signature of a streaming RPC from the client's
// perspective. Interceptors may wrap StreamingClientFuncs.
type StreamingClientFunc func(context.Context, Spec) StreamingClientConn
// StreamingHandlerFunc is the generic signature of a streaming RPC from the
// handler's perspective. Interceptors may wrap StreamingHandlerFuncs.
type StreamingHandlerFunc func(context.Context, StreamingHandlerConn) error
// An Interceptor adds logic to a generated handler or client, like the
// decorators or middleware you may have seen in other libraries. Interceptors
// may mutate requests and responses, handle errors, retry, recover from panics,
// emit logs and metrics, or do nearly anything else.
//
// The returned functions must be safe to call concurrently.
type Interceptor interface {
WrapUnary(UnaryFunc) UnaryFunc
WrapStreamingClient(StreamingClientFunc) StreamingClientFunc
WrapStreamingHandler(StreamingHandlerFunc) StreamingHandlerFunc
}
// UnaryInterceptorFunc is a simple Interceptor implementation that only
// wraps unary RPCs. It has no effect on streaming RPCs.
type UnaryInterceptorFunc func(UnaryFunc) UnaryFunc
// WrapUnary implements [Interceptor] by applying the interceptor function.
func (f UnaryInterceptorFunc) WrapUnary(next UnaryFunc) UnaryFunc { return f(next) }
// WrapStreamingClient implements [Interceptor] with a no-op.
func (f UnaryInterceptorFunc) WrapStreamingClient(next StreamingClientFunc) StreamingClientFunc {
return next
}
// WrapStreamingHandler implements [Interceptor] with a no-op.
func (f UnaryInterceptorFunc) WrapStreamingHandler(next StreamingHandlerFunc) StreamingHandlerFunc {
return next
}
// A chain composes multiple interceptors into one.
type chain struct {
interceptors []Interceptor
}
// newChain composes multiple interceptors into one.
func newChain(interceptors []Interceptor) *chain {
// We usually wrap in reverse order to have the first interceptor from
// the slice act first. Rather than doing this dance repeatedly, reverse the
// interceptor order now.
var chain chain
for i := len(interceptors) - 1; i >= 0; i-- {
if interceptor := interceptors[i]; interceptor != nil {
chain.interceptors = append(chain.interceptors, interceptor)
}
}
return &chain
}
func (c *chain) WrapUnary(next UnaryFunc) UnaryFunc {
for _, interceptor := range c.interceptors {
next = unaryThunk(next)
next = interceptor.WrapUnary(next)
}
return next
}
func (c *chain) WrapStreamingClient(next StreamingClientFunc) StreamingClientFunc {
for _, interceptor := range c.interceptors {
next = streamingClientThunk(next)
next = interceptor.WrapStreamingClient(next)
}
return next
}
func (c *chain) WrapStreamingHandler(next StreamingHandlerFunc) StreamingHandlerFunc {
for _, interceptor := range c.interceptors {
next = interceptor.WrapStreamingHandler(next)
}
return next
}
func unaryThunk(next UnaryFunc) UnaryFunc {
return func(ctx context.Context, req AnyRequest) (AnyResponse, error) {
if err := checkSentinel(ctx); err != nil {
return nil, err
}
return next(ctx, req)
}
}
func streamingClientThunk(next StreamingClientFunc) StreamingClientFunc {
return func(ctx context.Context, spec Spec) StreamingClientConn {
if err := checkSentinel(ctx); err != nil {
return &errStreamingClientConn{err: err}
}
return next(ctx, spec)
}
}
func checkSentinel(ctx context.Context) error {
if ctx.Value(clientCallInfoContextKey{}) != ctx.Value(sentinelContextKey{}) {
return errNewClientContextProhibited
}
return nil
}
@@ -1,165 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc (unknown)
// source: connectext/grpc/status/v1/status.proto
// This package is for internal use by Connect, and provides no backward
// compatibility guarantees whatsoever.
package statusv1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
anypb "google.golang.org/protobuf/types/known/anypb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// See https://cloud.google.com/apis/design/errors.
//
// This struct must remain binary-compatible with
// https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto.
type Status struct {
state protoimpl.MessageState `protogen:"open.v1"`
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` // a google.rpc.Code
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // developer-facing, English (localize in details or client-side)
Details []*anypb.Any `protobuf:"bytes,3,rep,name=details,proto3" json:"details,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Status) Reset() {
*x = Status{}
mi := &file_connectext_grpc_status_v1_status_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Status) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Status) ProtoMessage() {}
func (x *Status) ProtoReflect() protoreflect.Message {
mi := &file_connectext_grpc_status_v1_status_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Status.ProtoReflect.Descriptor instead.
func (*Status) Descriptor() ([]byte, []int) {
return file_connectext_grpc_status_v1_status_proto_rawDescGZIP(), []int{0}
}
func (x *Status) GetCode() int32 {
if x != nil {
return x.Code
}
return 0
}
func (x *Status) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *Status) GetDetails() []*anypb.Any {
if x != nil {
return x.Details
}
return nil
}
var File_connectext_grpc_status_v1_status_proto protoreflect.FileDescriptor
const file_connectext_grpc_status_v1_status_proto_rawDesc = "" +
"\n" +
"&connectext/grpc/status/v1/status.proto\x12\x0egrpc.status.v1\x1a\x19google/protobuf/any.proto\"f\n" +
"\x06Status\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12.\n" +
"\adetails\x18\x03 \x03(\v2\x14.google.protobuf.AnyR\adetailsB\xc3\x01\n" +
"\x12com.grpc.status.v1B\vStatusProtoP\x01ZFconnectrpc.com/connect/internal/gen/connectext/grpc/status/v1;statusv1\xa2\x02\x03GSX\xaa\x02\x0eGrpc.Status.V1\xca\x02\x0eGrpc\\Status\\V1\xe2\x02\x1aGrpc\\Status\\V1\\GPBMetadata\xea\x02\x10Grpc::Status::V1b\x06proto3"
var (
file_connectext_grpc_status_v1_status_proto_rawDescOnce sync.Once
file_connectext_grpc_status_v1_status_proto_rawDescData []byte
)
func file_connectext_grpc_status_v1_status_proto_rawDescGZIP() []byte {
file_connectext_grpc_status_v1_status_proto_rawDescOnce.Do(func() {
file_connectext_grpc_status_v1_status_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_connectext_grpc_status_v1_status_proto_rawDesc), len(file_connectext_grpc_status_v1_status_proto_rawDesc)))
})
return file_connectext_grpc_status_v1_status_proto_rawDescData
}
var file_connectext_grpc_status_v1_status_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_connectext_grpc_status_v1_status_proto_goTypes = []any{
(*Status)(nil), // 0: grpc.status.v1.Status
(*anypb.Any)(nil), // 1: google.protobuf.Any
}
var file_connectext_grpc_status_v1_status_proto_depIdxs = []int32{
1, // 0: grpc.status.v1.Status.details:type_name -> google.protobuf.Any
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_connectext_grpc_status_v1_status_proto_init() }
func file_connectext_grpc_status_v1_status_proto_init() {
if File_connectext_grpc_status_v1_status_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_connectext_grpc_status_v1_status_proto_rawDesc), len(file_connectext_grpc_status_v1_status_proto_rawDesc)),
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_connectext_grpc_status_v1_status_proto_goTypes,
DependencyIndexes: file_connectext_grpc_status_v1_status_proto_depIdxs,
MessageInfos: file_connectext_grpc_status_v1_status_proto_msgTypes,
}.Build()
File_connectext_grpc_status_v1_status_proto = out.File
file_connectext_grpc_status_v1_status_proto_goTypes = nil
file_connectext_grpc_status_v1_status_proto_depIdxs = nil
}
-647
View File
@@ -1,647 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"compress/gzip"
"context"
"io"
"net/http"
)
// A ClientOption configures a [Client].
//
// In addition to any options grouped in the documentation below, remember that
// any [Option] is also a valid ClientOption.
type ClientOption interface {
applyToClient(*clientConfig)
}
// WithAcceptCompression makes a compression algorithm available to a client.
// Clients ask servers to compress responses using any of the registered
// algorithms. The first registered algorithm is treated as the least
// preferred, and the last registered algorithm is the most preferred.
//
// It's safe to use this option liberally: servers will ignore any
// compression algorithms they don't support. To compress requests, pair this
// option with [WithSendCompression]. To remove support for a
// previously-registered compression algorithm, use WithAcceptCompression with
// nil decompressor and compressor constructors.
//
// Clients accept gzipped responses by default, using a compressor backed by the
// standard library's [gzip] package with the default compression level. Use
// [WithSendGzip] to compress requests with gzip.
//
// Calling WithAcceptCompression with an empty name is a no-op.
func WithAcceptCompression(
name string,
newDecompressor func() Decompressor,
newCompressor func() Compressor,
) ClientOption {
return &compressionOption{
Name: name,
CompressionPool: newCompressionPool(newDecompressor, newCompressor),
}
}
// WithClientOptions composes multiple ClientOptions into one.
func WithClientOptions(options ...ClientOption) ClientOption {
return &clientOptionsOption{options}
}
// WithGRPC configures clients to use the HTTP/2 gRPC protocol.
func WithGRPC() ClientOption {
return &grpcOption{web: false}
}
// WithGRPCWeb configures clients to use the gRPC-Web protocol.
func WithGRPCWeb() ClientOption {
return &grpcOption{web: true}
}
// WithProtoJSON configures a client to send JSON-encoded data instead of
// binary Protobuf. It uses the standard Protobuf JSON mapping as implemented
// by [google.golang.org/protobuf/encoding/protojson]: fields are named using
// lowerCamelCase, zero values are omitted, missing required fields are errors,
// enums are emitted as strings, etc.
func WithProtoJSON() ClientOption {
return WithCodec(&protoJSONCodec{codecNameJSON})
}
// WithSendCompression configures the client to use the specified algorithm to
// compress request messages. If the algorithm has not been registered using
// [WithAcceptCompression], the client will return errors at runtime.
//
// Because some servers don't support compression, clients default to sending
// uncompressed requests.
func WithSendCompression(name string) ClientOption {
return &sendCompressionOption{Name: name}
}
// WithSendGzip configures the client to gzip requests. Since clients have
// access to a gzip compressor by default, WithSendGzip doesn't require
// [WithSendCompression].
//
// Some servers don't support gzip, so clients default to sending uncompressed
// requests.
func WithSendGzip() ClientOption {
return WithSendCompression(compressionGzip)
}
// A HandlerOption configures a [Handler].
//
// In addition to any options grouped in the documentation below, remember that
// any [Option] is also a HandlerOption.
type HandlerOption interface {
applyToHandler(*handlerConfig)
}
// WithCompression configures handlers to support a compression algorithm.
// Clients may send messages compressed with that algorithm and/or request
// compressed responses. The [Compressor] and [Decompressor] produced by the
// supplied constructors must use the same algorithm. Internally, Connect pools
// compressors and decompressors.
//
// By default, handlers support gzip using the standard library's
// [compress/gzip] package at the default compression level. To remove support for
// a previously-registered compression algorithm, use WithCompression with nil
// decompressor and compressor constructors.
//
// Calling WithCompression with an empty name is a no-op.
func WithCompression(
name string,
newDecompressor func() Decompressor,
newCompressor func() Compressor,
) HandlerOption {
return &compressionOption{
Name: name,
CompressionPool: newCompressionPool(newDecompressor, newCompressor),
}
}
// WithHandlerOptions composes multiple HandlerOptions into one.
func WithHandlerOptions(options ...HandlerOption) HandlerOption {
return &handlerOptionsOption{options}
}
// WithRecover adds an interceptor that recovers from panics. The supplied
// function receives the context, [Spec], request headers, and the recovered
// value (which may be nil). It must return an error to send back to the
// client. It may also log the panic, emit metrics, or execute other
// error-handling logic. Handler functions must be safe to call concurrently.
//
// To preserve compatibility with [net/http]'s semantics, this interceptor
// doesn't handle panics with [http.ErrAbortHandler].
//
// By default, handlers don't recover from panics. Because the standard
// library's [http.Server] recovers from panics by default, this option isn't
// usually necessary to prevent crashes. Instead, it helps servers collect
// RPC-specific data during panics and send a more detailed error to
// clients.
func WithRecover(handle func(context.Context, Spec, http.Header, any) error) HandlerOption {
return WithInterceptors(&recoverHandlerInterceptor{handle: handle})
}
// WithRequireConnectProtocolHeader configures the Handler to require requests
// using the Connect RPC protocol to include the Connect-Protocol-Version
// header. This ensures that HTTP proxies and net/http middleware can easily
// identify valid Connect requests, even if they use a common Content-Type like
// application/json. However, it makes ad-hoc requests with tools like cURL
// more laborious. Streaming requests are not affected by this option.
//
// This option has no effect if the client uses the gRPC or gRPC-Web protocols.
func WithRequireConnectProtocolHeader() HandlerOption {
return &requireConnectProtocolHeaderOption{}
}
// WithConditionalHandlerOptions allows procedures in the same service to have
// different configurations: for example, one procedure may need a much larger
// WithReadMaxBytes setting than the others.
//
// WithConditionalHandlerOptions takes a function which may inspect each
// procedure's Spec before deciding which options to apply. Returning a nil
// slice is safe.
func WithConditionalHandlerOptions(conditional func(spec Spec) []HandlerOption) HandlerOption {
return &conditionalHandlerOptions{conditional: conditional}
}
// Option implements both [ClientOption] and [HandlerOption], so it can be
// applied both client-side and server-side.
type Option interface {
ClientOption
HandlerOption
}
// WithSchema provides a parsed representation of the schema for an RPC to a
// client or handler. The supplied schema is exposed as [Spec].Schema. This
// option is typically added by generated code.
//
// For services using protobuf schemas, the supplied schema should be a
// [google.golang.org/protobuf/reflect/protoreflect.MethodDescriptor].
func WithSchema(schema any) Option {
return &schemaOption{Schema: schema}
}
// WithRequestInitializer provides a function that initializes a new message.
// It may be used to dynamically construct request messages. It is called on
// server receives to construct the message to be unmarshaled into. The message
// will be a non nil pointer to the type created by the handler. Use the Schema
// field of the [Spec] to determine the type of the message.
func WithRequestInitializer(initializer func(spec Spec, message any) error) HandlerOption {
return &initializerOption{Initializer: initializer}
}
// WithResponseInitializer provides a function that initializes a new message.
// It may be used to dynamically construct response messages. It is called on
// client receives to construct the message to be unmarshaled into. The message
// will be a non nil pointer to the type created by the client. Use the Schema
// field of the [Spec] to determine the type of the message.
func WithResponseInitializer(initializer func(spec Spec, message any) error) ClientOption {
return &initializerOption{Initializer: initializer}
}
// WithCodec registers a serialization method with a client or handler.
// Handlers may have multiple codecs registered, and use whichever the client
// chooses. Clients may only have a single codec.
//
// By default, handlers and clients support binary Protocol Buffer data using
// [google.golang.org/protobuf/proto]. Handlers also support JSON by default,
// using the standard Protobuf JSON mapping. Users with more specialized needs
// may override the default codecs by registering a new codec under the "proto"
// or "json" names. When supplying a custom "proto" codec, keep in mind that
// some unexported, protocol-specific messages are serialized using Protobuf -
// take care to fall back to the standard Protobuf implementation if
// necessary.
//
// Registering a codec with an empty name is a no-op.
func WithCodec(codec Codec) Option {
return &codecOption{Codec: codec}
}
// WithCompressMinBytes sets a minimum size threshold for compression:
// regardless of compressor configuration, messages smaller than the configured
// minimum are sent uncompressed.
//
// The default minimum is zero. Setting a minimum compression threshold may
// improve overall performance, because the CPU cost of compressing very small
// messages usually isn't worth the small reduction in network I/O.
func WithCompressMinBytes(minBytes int) Option {
return &compressMinBytesOption{Min: minBytes}
}
// WithReadMaxBytes limits the performance impact of pathologically large
// messages sent by the other party. For handlers, WithReadMaxBytes limits the size
// of a message that the client can send. For clients, WithReadMaxBytes limits the
// size of a message that the server can respond with. Limits apply to each Protobuf
// message, not to the stream as a whole.
//
// Setting WithReadMaxBytes to zero allows any message size. Both clients and
// handlers default to allowing any request size.
//
// Handlers may also use [http.MaxBytesHandler] to limit the total size of the
// HTTP request stream (rather than the per-message size). Connect handles
// [http.MaxBytesError] specially, so clients still receive errors with the
// appropriate error code and informative messages.
func WithReadMaxBytes(maxBytes int) Option {
return &readMaxBytesOption{Max: maxBytes}
}
// WithSendMaxBytes prevents sending messages too large for the client/handler
// to handle without significant performance overhead. For handlers, WithSendMaxBytes
// limits the size of a message that the handler can respond with. For clients,
// WithSendMaxBytes limits the size of a message that the client can send. Limits
// apply to each message, not to the stream as a whole.
//
// Setting WithSendMaxBytes to zero allows any message size. Both clients and
// handlers default to allowing any message size.
func WithSendMaxBytes(maxBytes int) Option {
return &sendMaxBytesOption{Max: maxBytes}
}
// WithIdempotency declares the idempotency of the procedure. This can determine
// whether a procedure call can safely be retried, and may affect which request
// modalities are allowed for a given procedure call.
//
// In most cases, you should not need to manually set this. It is normally set
// by the code generator for your schema. For protobuf schemas, it can be set like this:
//
// rpc Ping(PingRequest) returns (PingResponse) {
// option idempotency_level = NO_SIDE_EFFECTS;
// }
func WithIdempotency(idempotencyLevel IdempotencyLevel) Option {
return &idempotencyOption{idempotencyLevel: idempotencyLevel}
}
// WithHTTPGet allows Connect-protocol clients to use HTTP GET requests for
// side-effect free unary RPC calls. Typically, the service schema indicates
// which procedures are idempotent (see [WithIdempotency] for an example
// protobuf schema). The gRPC and gRPC-Web protocols are POST-only, so this
// option has no effect when combined with [WithGRPC] or [WithGRPCWeb].
//
// Using HTTP GET requests makes it easier to take advantage of CDNs, caching
// reverse proxies, and browsers' built-in caching. Note, however, that servers
// don't automatically set any cache headers; you can set cache headers using
// interceptors or by adding headers in individual procedure implementations.
//
// By default, all requests are made as HTTP POSTs.
func WithHTTPGet() ClientOption {
return &enableGet{}
}
// WithInterceptors configures a client or handler's interceptor stack. Repeated
// WithInterceptors options are applied in order, so
//
// WithInterceptors(A) + WithInterceptors(B, C) == WithInterceptors(A, B, C)
//
// Unary interceptors compose like an onion. The first interceptor provided is
// the outermost layer of the onion: it acts first on the context and request,
// and last on the response and error.
//
// Stream interceptors also behave like an onion: the first interceptor
// provided is the outermost wrapper for the [StreamingClientConn] or
// [StreamingHandlerConn]. It's the first to see sent messages and the last to
// see received messages.
//
// Applied to client and handler, WithInterceptors(A, B, ..., Y, Z) produces:
//
// client.Send() client.Receive()
// | ^
// v |
// A --- --- A
// B --- --- B
// : ... ... :
// Y --- --- Y
// Z --- --- Z
// | ^
// v |
// = = = = = = = = = = = = = = = =
// network
// = = = = = = = = = = = = = = = =
// | ^
// v |
// A --- --- A
// B --- --- B
// : ... ... :
// Y --- --- Y
// Z --- --- Z
// | ^
// v |
// handler.Receive() handler.Send()
// | ^
// | |
// '-> handler logic >-'
//
// Note that in clients, Send handles the request message(s) and Receive
// handles the response message(s). For handlers, it's the reverse. Depending
// on your interceptor's logic, you may need to wrap one method in clients and
// the other in handlers.
func WithInterceptors(interceptors ...Interceptor) Option {
return &interceptorsOption{interceptors}
}
// WithOptions composes multiple Options into one.
func WithOptions(options ...Option) Option {
return &optionsOption{options}
}
type schemaOption struct {
Schema any
}
func (o *schemaOption) applyToClient(config *clientConfig) {
config.Schema = o.Schema
}
func (o *schemaOption) applyToHandler(config *handlerConfig) {
config.Schema = o.Schema
}
type initializerOption struct {
Initializer func(spec Spec, message any) error
}
func (o *initializerOption) applyToHandler(config *handlerConfig) {
config.Initializer = maybeInitializer{initializer: o.Initializer}
}
func (o *initializerOption) applyToClient(config *clientConfig) {
config.Initializer = maybeInitializer{initializer: o.Initializer}
}
type maybeInitializer struct {
initializer func(spec Spec, message any) error
}
func (o maybeInitializer) maybe(spec Spec, message any) error {
if o.initializer != nil {
return o.initializer(spec, message)
}
return nil
}
type clientOptionsOption struct {
options []ClientOption
}
func (o *clientOptionsOption) applyToClient(config *clientConfig) {
for _, option := range o.options {
option.applyToClient(config)
}
}
type codecOption struct {
Codec Codec
}
func (o *codecOption) applyToClient(config *clientConfig) {
if o.Codec == nil || o.Codec.Name() == "" {
return
}
config.Codec = o.Codec
}
func (o *codecOption) applyToHandler(config *handlerConfig) {
if o.Codec == nil || o.Codec.Name() == "" {
return
}
config.Codecs[o.Codec.Name()] = o.Codec
}
type compressionOption struct {
Name string
CompressionPool *compressionPool
}
func (o *compressionOption) applyToClient(config *clientConfig) {
o.apply(&config.CompressionNames, config.CompressionPools)
}
func (o *compressionOption) applyToHandler(config *handlerConfig) {
o.apply(&config.CompressionNames, config.CompressionPools)
}
func (o *compressionOption) apply(configuredNames *[]string, configuredPools map[string]*compressionPool) {
if o.Name == "" {
return
}
if o.CompressionPool == nil {
delete(configuredPools, o.Name)
var names []string
for _, name := range *configuredNames {
if name == o.Name {
continue
}
names = append(names, name)
}
*configuredNames = names
return
}
configuredPools[o.Name] = o.CompressionPool
*configuredNames = append(*configuredNames, o.Name)
}
type compressMinBytesOption struct {
Min int
}
func (o *compressMinBytesOption) applyToClient(config *clientConfig) {
config.CompressMinBytes = o.Min
}
func (o *compressMinBytesOption) applyToHandler(config *handlerConfig) {
config.CompressMinBytes = o.Min
}
type readMaxBytesOption struct {
Max int
}
func (o *readMaxBytesOption) applyToClient(config *clientConfig) {
config.ReadMaxBytes = o.Max
}
func (o *readMaxBytesOption) applyToHandler(config *handlerConfig) {
config.ReadMaxBytes = o.Max
}
type sendMaxBytesOption struct {
Max int
}
func (o *sendMaxBytesOption) applyToClient(config *clientConfig) {
config.SendMaxBytes = o.Max
}
func (o *sendMaxBytesOption) applyToHandler(config *handlerConfig) {
config.SendMaxBytes = o.Max
}
type handlerOptionsOption struct {
options []HandlerOption
}
func (o *handlerOptionsOption) applyToHandler(config *handlerConfig) {
for _, option := range o.options {
option.applyToHandler(config)
}
}
type requireConnectProtocolHeaderOption struct{}
func (o *requireConnectProtocolHeaderOption) applyToHandler(config *handlerConfig) {
config.RequireConnectProtocolHeader = true
}
type idempotencyOption struct {
idempotencyLevel IdempotencyLevel
}
func (o *idempotencyOption) applyToClient(config *clientConfig) {
config.IdempotencyLevel = o.idempotencyLevel
}
func (o *idempotencyOption) applyToHandler(config *handlerConfig) {
config.IdempotencyLevel = o.idempotencyLevel
}
type grpcOption struct {
web bool
}
func (o *grpcOption) applyToClient(config *clientConfig) {
config.Protocol = &protocolGRPC{web: o.web}
}
type enableGet struct{}
func (o *enableGet) applyToClient(config *clientConfig) {
config.EnableGet = true
}
// WithHTTPGetMaxURLSize sets the maximum allowable URL length for GET requests
// made using the Connect protocol. It has no effect on gRPC or gRPC-Web
// clients, since those protocols are POST-only.
//
// Limiting the URL size is useful as most user agents, proxies, and servers
// have limits on the allowable length of a URL. For example, Apache and Nginx
// limit the size of a request line to around 8 KiB, meaning that maximum
// length of a URL is a bit smaller than this. If you run into URL size
// limitations imposed by your network infrastructure and don't know the
// maximum allowable size, or if you'd prefer to be cautious from the start, a
// 4096 byte (4 KiB) limit works with most common proxies and CDNs.
//
// If fallback is set to true and the URL would be longer than the configured
// maximum value, the request will be sent as an HTTP POST instead. If fallback
// is set to false, the request will fail with [CodeResourceExhausted].
//
// By default, Connect-protocol clients with GET requests enabled may send a
// URL of any size.
func WithHTTPGetMaxURLSize(bytes int, fallback bool) ClientOption {
return &getURLMaxBytes{Max: bytes, Fallback: fallback}
}
type getURLMaxBytes struct {
Max int
Fallback bool
}
func (o *getURLMaxBytes) applyToClient(config *clientConfig) {
config.GetURLMaxBytes = o.Max
config.GetUseFallback = o.Fallback
}
type interceptorsOption struct {
Interceptors []Interceptor
}
func (o *interceptorsOption) applyToClient(config *clientConfig) {
config.Interceptor = o.chainWith(config.Interceptor)
}
func (o *interceptorsOption) applyToHandler(config *handlerConfig) {
config.Interceptor = o.chainWith(config.Interceptor)
}
func (o *interceptorsOption) chainWith(current Interceptor) Interceptor {
if len(o.Interceptors) == 0 {
return current
}
if current == nil && len(o.Interceptors) == 1 {
return o.Interceptors[0]
}
if current == nil && len(o.Interceptors) > 1 {
return newChain(o.Interceptors)
}
return newChain(append([]Interceptor{current}, o.Interceptors...))
}
type optionsOption struct {
options []Option
}
func (o *optionsOption) applyToClient(config *clientConfig) {
for _, option := range o.options {
option.applyToClient(config)
}
}
func (o *optionsOption) applyToHandler(config *handlerConfig) {
for _, option := range o.options {
option.applyToHandler(config)
}
}
type sendCompressionOption struct {
Name string
}
func (o *sendCompressionOption) applyToClient(config *clientConfig) {
config.RequestCompressionName = o.Name
}
func withGzip() Option {
return &compressionOption{
Name: compressionGzip,
CompressionPool: newCompressionPool(
func() Decompressor { return &gzip.Reader{} },
func() Compressor { return gzip.NewWriter(io.Discard) },
),
}
}
func withProtoBinaryCodec() Option {
return WithCodec(&protoBinaryCodec{})
}
func withProtoJSONCodecs() HandlerOption {
return WithHandlerOptions(
WithCodec(&protoJSONCodec{codecNameJSON}),
WithCodec(&protoJSONCodec{codecNameJSONCharsetUTF8}),
)
}
type conditionalHandlerOptions struct {
conditional func(spec Spec) []HandlerOption
}
func (o *conditionalHandlerOptions) applyToHandler(config *handlerConfig) {
spec := config.newSpec()
if spec.Procedure == "" {
return // ignore empty specs
}
for _, option := range o.conditional(spec) {
option.applyToHandler(config)
}
}
-42
View File
@@ -1,42 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"strings"
)
// extractProtoPath returns the trailing portion of the URL's path,
// corresponding to the Protobuf package, service, and method. It always starts
// with a slash. Within connect, we use this as (1) Spec.Procedure and (2) the
// path when mounting handlers on muxes.
func extractProtoPath(path string) string {
segments := strings.Split(path, "/")
var pkg, method string
if len(segments) > 0 {
pkg = segments[0]
}
if len(segments) > 1 {
pkg = segments[len(segments)-2]
method = segments[len(segments)-1]
}
if pkg == "" {
return "/"
}
if method == "" {
return "/" + pkg
}
return "/" + pkg + "/" + method
}
-424
View File
@@ -1,424 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"context"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"sort"
"strings"
)
// The names of the Connect, gRPC, and gRPC-Web protocols (as exposed by
// [Peer].Protocol). Additional protocols may be added in the future.
const (
ProtocolConnect = "connect"
ProtocolGRPC = "grpc"
ProtocolGRPCWeb = "grpcweb"
)
const (
headerContentType = "Content-Type"
headerContentEncoding = "Content-Encoding"
headerContentLength = "Content-Length"
headerHost = "Host"
headerUserAgent = "User-Agent"
headerTrailer = "Trailer"
headerDate = "Date"
discardLimit = 1024 * 1024 * 4 // 4MiB
)
var errNoTimeout = errors.New("no timeout")
// A Protocol defines the HTTP semantics to use when sending and receiving
// messages. It ties together codecs, compressors, and net/http to produce
// Senders and Receivers.
//
// For example, connect supports the gRPC protocol using this abstraction. Among
// many other things, the protocol implementation is responsible for
// translating timeouts from Go contexts to HTTP and vice versa. For gRPC, it
// converts timeouts to and from strings (for example, 10*time.Second <->
// "10S"), and puts those strings into the "Grpc-Timeout" HTTP header. Other
// protocols might encode durations differently, put them into a different HTTP
// header, or ignore them entirely.
//
// We don't have any short-term plans to export this interface; it's just here
// to separate the protocol-specific portions of connect from the
// protocol-agnostic plumbing.
type protocol interface {
NewHandler(*protocolHandlerParams) protocolHandler
NewClient(*protocolClientParams) (protocolClient, error)
}
// HandlerParams are the arguments provided to a Protocol's NewHandler
// method, bundled into a struct to allow backward-compatible argument
// additions. Protocol implementations should take care to use the supplied
// Spec rather than constructing their own, since new fields may have been
// added.
type protocolHandlerParams struct {
Spec Spec
Codecs readOnlyCodecs
CompressionPools readOnlyCompressionPools
CompressMinBytes int
BufferPool *bufferPool
ReadMaxBytes int
SendMaxBytes int
RequireConnectProtocolHeader bool
IdempotencyLevel IdempotencyLevel
}
// Handler is the server side of a protocol. HTTP handlers typically support
// multiple protocols, codecs, and compressors.
type protocolHandler interface {
// Methods is the set of HTTP methods the protocol can handle.
Methods() map[string]struct{}
// ContentTypes is the set of HTTP Content-Types that the protocol can
// handle.
ContentTypes() map[string]struct{}
// SetTimeout runs before NewStream. Implementations may inspect the HTTP
// request, parse any timeout set by the client, and return a modified
// context and cancellation function.
//
// If the client didn't send a timeout, SetTimeout should return the
// request's context, a nil cancellation function, and a nil error.
SetTimeout(*http.Request) (context.Context, context.CancelFunc, error)
// CanHandlePayload returns true if the protocol can handle an HTTP request.
// This is called after the request method is validated, so we only need to
// be concerned with the content type/payload specifically.
CanHandlePayload(*http.Request, string) bool
// NewConn constructs a HandlerConn for the message exchange.
NewConn(http.ResponseWriter, *http.Request) (handlerConnCloser, bool)
}
// ClientParams are the arguments provided to a Protocol's NewClient method,
// bundled into a struct to allow backward-compatible argument additions.
// Protocol implementations should take care to use the supplied Spec rather
// than constructing their own, since new fields may have been added.
type protocolClientParams struct {
CompressionName string
CompressionPools readOnlyCompressionPools
Codec Codec
CompressMinBytes int
HTTPClient HTTPClient
URL *url.URL
BufferPool *bufferPool
ReadMaxBytes int
SendMaxBytes int
EnableGet bool
GetURLMaxBytes int
GetUseFallback bool
// The gRPC family of protocols always needs access to a Protobuf codec to
// marshal and unmarshal errors.
Protobuf Codec
}
// Client is the client side of a protocol. HTTP clients typically use a single
// protocol, codec, and compressor to send requests.
type protocolClient interface {
// Peer describes the server for the RPC.
Peer() Peer
// WriteRequestHeader writes any protocol-specific request headers.
WriteRequestHeader(StreamType, http.Header)
// NewConn constructs a StreamingClientConn for the message exchange.
//
// Implementations should assume that the supplied HTTP headers have already
// been populated by WriteRequestHeader. When constructing a stream for a
// unary call, implementations may assume that the Sender's Send and Close
// methods return before the Receiver's Receive or Close methods are called.
NewConn(context.Context, Spec, http.Header) streamingClientConn
}
// streamingClientConn extends StreamingClientConn with a method for registering
// a hook when the HTTP request is actually sent.
type streamingClientConn interface {
StreamingClientConn
onRequestSend(fn func(*http.Request))
}
// errorTranslatingHandlerConnCloser wraps a handlerConnCloser to ensure that
// we always return coded errors to users and write coded errors to the
// network.
//
// It's used in protocol implementations.
type errorTranslatingHandlerConnCloser struct {
handlerConnCloser
toWire func(error) error
fromWire func(error) error
}
func (hc *errorTranslatingHandlerConnCloser) Send(msg any) error {
return hc.fromWire(hc.handlerConnCloser.Send(msg))
}
func (hc *errorTranslatingHandlerConnCloser) Receive(msg any) error {
return hc.fromWire(hc.handlerConnCloser.Receive(msg))
}
func (hc *errorTranslatingHandlerConnCloser) Close(err error) error {
closeErr := hc.handlerConnCloser.Close(hc.toWire(err))
return hc.fromWire(closeErr)
}
func (hc *errorTranslatingHandlerConnCloser) getHTTPMethod() string {
if methoder, ok := hc.handlerConnCloser.(interface{ getHTTPMethod() string }); ok {
return methoder.getHTTPMethod()
}
return http.MethodPost
}
// errorTranslatingClientConn wraps a StreamingClientConn to make sure that we always
// return coded errors from clients.
//
// It's used in protocol implementations.
type errorTranslatingClientConn struct {
streamingClientConn
fromWire func(error) error
}
func (cc *errorTranslatingClientConn) Send(msg any) error {
return cc.fromWire(cc.streamingClientConn.Send(msg))
}
func (cc *errorTranslatingClientConn) Receive(msg any) error {
return cc.fromWire(cc.streamingClientConn.Receive(msg))
}
func (cc *errorTranslatingClientConn) CloseRequest() error {
return cc.fromWire(cc.streamingClientConn.CloseRequest())
}
func (cc *errorTranslatingClientConn) CloseResponse() error {
return cc.fromWire(cc.streamingClientConn.CloseResponse())
}
func (cc *errorTranslatingClientConn) onRequestSend(fn func(*http.Request)) {
cc.streamingClientConn.onRequestSend(fn)
}
// wrapHandlerConnWithCodedErrors ensures that we (1) automatically code
// context-related errors correctly when writing them to the network, and (2)
// return *Errors from all exported APIs.
func wrapHandlerConnWithCodedErrors(conn handlerConnCloser) handlerConnCloser {
return &errorTranslatingHandlerConnCloser{
handlerConnCloser: conn,
toWire: wrapIfContextError,
fromWire: wrapIfUncoded,
}
}
// wrapClientConnWithCodedErrors ensures that we always return *Errors from
// public APIs.
func wrapClientConnWithCodedErrors(conn streamingClientConn) streamingClientConn {
return &errorTranslatingClientConn{
streamingClientConn: conn,
fromWire: wrapIfUncoded,
}
}
func mappedMethodHandlers(handlers []protocolHandler) map[string][]protocolHandler {
methodHandlers := make(map[string][]protocolHandler)
for _, handler := range handlers {
for method := range handler.Methods() {
methodHandlers[method] = append(methodHandlers[method], handler)
}
}
return methodHandlers
}
func sortedAcceptPostValue(handlers []protocolHandler) string {
contentTypes := make(map[string]struct{})
for _, handler := range handlers {
for contentType := range handler.ContentTypes() {
contentTypes[contentType] = struct{}{}
}
}
accept := make([]string, 0, len(contentTypes))
for ct := range contentTypes {
accept = append(accept, ct)
}
sort.Strings(accept)
return strings.Join(accept, ", ")
}
func sortedAllowMethodValue(handlers []protocolHandler) string {
methods := make(map[string]struct{})
for _, handler := range handlers {
for method := range handler.Methods() {
methods[method] = struct{}{}
}
}
allow := make([]string, 0, len(methods))
for ct := range methods {
allow = append(allow, ct)
}
sort.Strings(allow)
return strings.Join(allow, ", ")
}
func isCommaOrSpace(c rune) bool {
return c == ',' || c == ' '
}
func discard(reader io.Reader) (int64, error) {
if lr, ok := reader.(*io.LimitedReader); ok {
return io.Copy(io.Discard, lr)
}
// We don't want to get stuck throwing data away forever, so limit how much
// we're willing to do here.
lr := &io.LimitedReader{R: reader, N: discardLimit}
return io.Copy(io.Discard, lr)
}
// negotiateCompression determines and validates the request compression and
// response compression using the available compressors and protocol-specific
// Content-Encoding and Accept-Encoding headers.
func negotiateCompression( //nolint:nonamedreturns
availableCompressors readOnlyCompressionPools,
sent, accept string,
) (requestCompression, responseCompression string, clientVisibleErr *Error) {
requestCompression = compressionIdentity
if sent != "" && sent != compressionIdentity {
// We default to identity, so we only care if the client sends something
// other than the empty string or compressIdentity.
if availableCompressors.Contains(sent) {
requestCompression = sent
} else {
// To comply with
// https://github.com/grpc/grpc/blob/master/doc/compression.md and the
// Connect protocol, we should return CodeUnimplemented and specify
// acceptable compression(s) (in addition to setting the a
// protocol-specific accept-encoding header).
return "", "", errorf(
CodeUnimplemented,
"unknown compression %q: supported encodings are %v",
sent, availableCompressors.CommaSeparatedNames(),
)
}
}
// Support asymmetric compression. This logic follows
// https://github.com/grpc/grpc/blob/master/doc/compression.md and common
// sense.
responseCompression = requestCompression
// If we're not already planning to compress the response, check whether the
// client requested a compression algorithm we support.
if responseCompression == compressionIdentity && accept != "" {
for _, name := range strings.FieldsFunc(accept, isCommaOrSpace) {
if availableCompressors.Contains(name) {
// We found a mutually supported compression algorithm. Unlike standard
// HTTP, there's no preference weighting, so can bail out immediately.
responseCompression = name
break
}
}
}
return requestCompression, responseCompression, nil
}
// checkServerStreamsCanFlush ensures that bidi and server streaming handlers
// have received an http.ResponseWriter that implements http.Flusher, since
// they must flush data after sending each message.
func checkServerStreamsCanFlush(spec Spec, responseWriter http.ResponseWriter) *Error {
requiresFlusher := (spec.StreamType & StreamTypeServer) == StreamTypeServer
if _, flushable := responseWriter.(http.Flusher); requiresFlusher && !flushable {
return NewError(CodeInternal, fmt.Errorf("%T does not implement http.Flusher", responseWriter))
}
return nil
}
func flushResponseWriter(w http.ResponseWriter) {
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
func canonicalizeContentType(contentType string) string {
// Typically, clients send Content-Type in canonical form, without
// parameters. In those cases, we'd like to avoid parsing and
// canonicalization overhead.
//
// See https://www.rfc-editor.org/rfc/rfc2045.html#section-5.1 for a full
// grammar.
var slashes int
for _, r := range contentType {
switch {
case r >= 'a' && r <= 'z':
case r == '.' || r == '+' || r == '-':
case r == '/':
slashes++
default:
return canonicalizeContentTypeSlow(contentType)
}
}
if slashes == 1 {
return contentType
}
return canonicalizeContentTypeSlow(contentType)
}
func canonicalizeContentTypeSlow(contentType string) string {
base, params, err := mime.ParseMediaType(contentType)
if err != nil {
return contentType
}
// According to RFC 9110 Section 8.3.2, the charset parameter value should be treated as case-insensitive.
// mime.FormatMediaType canonicalizes parameter names, but not parameter values,
// because the case sensitivity of a parameter value depends on its semantics.
// Therefore, the charset parameter value should be canonicalized here.
// ref.) https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.2
if charset, ok := params["charset"]; ok {
params["charset"] = strings.ToLower(charset)
}
return mime.FormatMediaType(base, params)
}
func httpToCode(httpCode int) Code {
// https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
// Note that this is NOT the inverse of the gRPC-to-HTTP or Connect-to-HTTP
// mappings.
// Literals are easier to compare to the specification (vs named
// constants).
switch httpCode {
case 400:
return CodeInternal
case 401:
return CodeUnauthenticated
case 403:
return CodePermissionDenied
case 404:
return CodeUnimplemented
case 429:
return CodeUnavailable
case 502, 503, 504:
return CodeUnavailable
default:
return CodeUnknown
}
}
-1449
View File
File diff suppressed because it is too large Load Diff
-1010
View File
File diff suppressed because it is too large Load Diff
-64
View File
@@ -1,64 +0,0 @@
// Copyright 2021-2025 The Connect Authors
//
// 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.
package connect
import (
"context"
"net/http"
)
// recoverHandlerInterceptor lets handlers trap panics, perform side effects
// (like emitting logs or metrics), and present a friendlier error message to
// clients.
type recoverHandlerInterceptor struct {
Interceptor
handle func(context.Context, Spec, http.Header, any) error
}
func (i *recoverHandlerInterceptor) WrapUnary(next UnaryFunc) UnaryFunc {
return func(ctx context.Context, req AnyRequest) (_ AnyResponse, retErr error) {
if req.Spec().IsClient {
return next(ctx, req)
}
defer func() {
if r := recover(); r != nil {
// net/http checks for ErrAbortHandler with ==, so we should too.
if r == http.ErrAbortHandler { //nolint:errorlint,goerr113
panic(r) //nolint:forbidigo
}
retErr = i.handle(ctx, req.Spec(), req.Header(), r)
}
}()
res, err := next(ctx, req)
return res, err
}
}
func (i *recoverHandlerInterceptor) WrapStreamingHandler(next StreamingHandlerFunc) StreamingHandlerFunc {
return func(ctx context.Context, conn StreamingHandlerConn) (retErr error) {
defer func() {
if r := recover(); r != nil {
// net/http checks for ErrAbortHandler with ==, so we should too.
if r == http.ErrAbortHandler { //nolint:errorlint,goerr113
panic(r) //nolint:forbidigo
}
retErr = i.handle(ctx, conn.Spec(), conn.RequestHeader(), r)
}
}()
err := next(ctx, conn)
return err
}
}
+7 -5
View File
@@ -1,5 +1,7 @@
package ansiterm
import "slices"
type csiEntryState struct {
baseState
}
@@ -13,11 +15,11 @@ func (csiState csiEntryState) Handle(b byte) (s state, e error) {
}
switch {
case sliceContains(alphabetics, b):
case slices.Contains(alphabetics, b):
return csiState.parser.ground, nil
case sliceContains(csiCollectables, b):
case slices.Contains(csiCollectables, b):
return csiState.parser.csiParam, nil
case sliceContains(executors, b):
case slices.Contains(executors, b):
return csiState, csiState.parser.execute()
}
@@ -33,9 +35,9 @@ func (csiState csiEntryState) Transition(s state) error {
return csiState.parser.csiDispatch()
case csiState.parser.csiParam:
switch {
case sliceContains(csiParams, csiState.parser.context.currentChar):
case slices.Contains(csiParams, csiState.parser.context.currentChar):
_ = csiState.parser.collectParam()
case sliceContains(intermeds, csiState.parser.context.currentChar):
case slices.Contains(intermeds, csiState.parser.context.currentChar):
_ = csiState.parser.collectInter()
}
}
+5 -3
View File
@@ -1,5 +1,7 @@
package ansiterm
import "slices"
type csiParamState struct {
baseState
}
@@ -13,11 +15,11 @@ func (csiState csiParamState) Handle(b byte) (s state, e error) {
}
switch {
case sliceContains(alphabetics, b):
case slices.Contains(alphabetics, b):
return csiState.parser.ground, nil
case sliceContains(csiCollectables, b):
case slices.Contains(csiCollectables, b):
return csiState, csiState.parser.collectParam()
case sliceContains(executors, b):
case slices.Contains(executors, b):
return csiState, csiState.parser.execute()
}
+5 -3
View File
@@ -1,5 +1,7 @@
package ansiterm
import "slices"
type escapeIntermediateState struct {
baseState
}
@@ -12,11 +14,11 @@ func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
}
switch {
case sliceContains(intermeds, b):
case slices.Contains(intermeds, b):
return escState, escState.parser.collectInter()
case sliceContains(executors, b):
case slices.Contains(executors, b):
return escState, escState.parser.execute()
case sliceContains(escapeIntermediateToGroundBytes, b):
case slices.Contains(escapeIntermediateToGroundBytes, b):
return escState.parser.ground, nil
}
+5 -3
View File
@@ -1,5 +1,7 @@
package ansiterm
import "slices"
type escapeState struct {
baseState
}
@@ -16,11 +18,11 @@ func (escState escapeState) Handle(b byte) (s state, e error) {
return escState.parser.csiEntry, nil
case b == ANSI_OSC_STRING_ENTRY:
return escState.parser.oscString, nil
case sliceContains(executors, b):
case slices.Contains(executors, b):
return escState, escState.parser.execute()
case sliceContains(escapeToGroundBytes, b):
case slices.Contains(escapeToGroundBytes, b):
return escState.parser.ground, nil
case sliceContains(intermeds, b):
case slices.Contains(intermeds, b):
return escState.parser.escapeIntermediate, nil
}
+4 -2
View File
@@ -1,5 +1,7 @@
package ansiterm
import "slices"
type groundState struct {
baseState
}
@@ -13,10 +15,10 @@ func (gs groundState) Handle(b byte) (s state, e error) {
}
switch {
case sliceContains(printables, b):
case slices.Contains(printables, b):
return gs, gs.parser.print()
case sliceContains(executors, b):
case slices.Contains(executors, b):
return gs, gs.parser.execute()
}
+4 -4
View File
@@ -20,12 +20,12 @@ type AnsiParser struct {
oscString state
stateMap []state
logf func(string, ...interface{})
logf func(string, ...any)
}
type Option func(*AnsiParser)
func WithLogf(f func(string, ...interface{})) Option {
func WithLogf(f func(string, ...any)) Option {
return func(ap *AnsiParser) {
ap.logf = f
}
@@ -45,7 +45,7 @@ func CreateParser(initialState string, evtHandler AnsiEventHandler, opts ...Opti
logger := log.New(logFile, "", log.LstdFlags)
if ap.logf != nil {
l := ap.logf
ap.logf = func(s string, v ...interface{}) {
ap.logf = func(s string, v ...any) {
l(s, v...)
logger.Printf(s, v...)
}
@@ -55,7 +55,7 @@ func CreateParser(initialState string, evtHandler AnsiEventHandler, opts ...Opti
}
if ap.logf == nil {
ap.logf = func(string, ...interface{}) {}
ap.logf = func(string, ...any) {}
}
ap.csiEntry = csiEntryState{baseState{name: "CsiEntry", parser: ap}}

Some files were not shown because too many files have changed in this diff Show More