pax_global_header00006660000000000000000000000064145347135160014523gustar00rootroot0000000000000052 comment=f4373c860058f7f23e10596285f1059c290f112f connect-go-1.13.0/000077500000000000000000000000001453471351600136415ustar00rootroot00000000000000connect-go-1.13.0/.github/000077500000000000000000000000001453471351600152015ustar00rootroot00000000000000connect-go-1.13.0/.github/CODE_OF_CONDUCT.md000066400000000000000000000002151453471351600177760ustar00rootroot00000000000000## Community Code of Conduct Connect follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). connect-go-1.13.0/.github/CONTRIBUTING.md000066400000000000000000000043611453471351600174360ustar00rootroot00000000000000Contributing ============ We'd love your help making Connect better! If you'd like to add new exported APIs, please [open an issue][open-issue] describing your proposal — discussing API changes ahead of time makes pull request review much smoother. In your issue, pull request, and any other communications, please remember to treat your fellow contributors with respect! Note that you'll need to sign the [Contributor License Agreement][cla] before we can accept any of your contributions. If necessary, a bot will remind you to accept the CLA when you open your pull request. ## Setup [Fork][fork], then clone the repository: ``` mkdir -p $GOPATH/src/connectrpc.com cd $GOPATH/src/connectrpc.com git clone git@github.com:your_github_username/connect-go.git connect cd connect git remote add upstream https://github.com/connectrpc/connect-go.git git fetch upstream ``` Make sure that the tests and the linters pass (you'll need `bash` and the latest stable Go release installed): ``` make ``` ## Making Changes Start by creating a new branch for your changes: ``` cd $GOPATH/src/connectrpc.com/connect git checkout main git fetch upstream git rebase upstream/main git checkout -b cool_new_feature ``` Make your changes, then ensure that `make` still passes. (Unless you're changing `protoc-gen-connect-go`, you can use the standard `go build ./...` and `go test ./...` while you're coding.) When you're satisfied with your changes, push them to your fork. ``` git commit -a git push origin cool_new_feature ``` Then use the GitHub UI to open a pull request. At this point, you're waiting on us to review your changes. We *try* to respond to issues and pull requests within a few business days, and we may suggest some improvements or alternatives. Once your changes are approved, one of the project maintainers will merge them. We're much more likely to approve your changes if you: * Add tests for new functionality. * Write a [good commit message][commit-message]. * Maintain backward compatibility. [fork]: https://github.com/connectrpc/connect-go/fork [open-issue]: https://github.com/connectrpc/connect-go/issues/new [cla]: https://cla-assistant.io/connectrpc/connect-go [commit-message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html connect-go-1.13.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001453471351600173645ustar00rootroot00000000000000connect-go-1.13.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000015531453471351600220620ustar00rootroot00000000000000--- name: Bug report about: Let us know about a bug title: '' labels: bug assignees: '' --- **Describe the bug** As clearly as you can, please tell us what the bug is. **To Reproduce** Help us to reproduce the buggy behavior. Ideally, you'd provide a self-contained test that shows us the bug: ```bash mkdir tmp && cd ./tmp go mod init example go get connectrpc.com/connect touch example_test.go ``` And in `example_test.go`: ```go package bugreport func TestThatReproducesBug(t *testing.T) { // your reproduction here } ``` **Environment (please complete the following information):** - `connect-go` version or commit: (for example, `v0.1.0` or `5bfc7a1b440ebffdc952d813332e3617ca611395`) - `go version`: (for example, `go version go1.18.3 darwin/amd64`) - your complete `go.mod`: ```go ``` **Additional context** Add any other context about the problem here. connect-go-1.13.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011461453471351600231130ustar00rootroot00000000000000--- name: Feature request about: Suggest a new feature or improvement title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** What's the problem? For example, "I'm always frustrated when..." **Describe the solution you'd like** What would you like to have `connect-go` do? How should we solve the problem? **Describe alternatives you've considered** If you've proposed a solution, are there any alternatives? Why are they worse than your preferred approach? **Additional context** Add any other context or screenshots about the feature request here. connect-go-1.13.0/.github/dependabot.yml000066400000000000000000000003411453471351600200270ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: gomod directory: "/internal/conformance" schedule: interval: "weekly" connect-go-1.13.0/.github/pull_request_template.md000066400000000000000000000002341453471351600221410ustar00rootroot00000000000000 connect-go-1.13.0/.github/release.yml000066400000000000000000000004221453471351600173420ustar00rootroot00000000000000changelog: exclude: labels: - ignore-for-release authors: - dependabot categories: - title: Enhancements labels: - enhancement - title: Bugfixes labels: - bug - title: Other changes labels: - "*" connect-go-1.13.0/.github/workflows/000077500000000000000000000000001453471351600172365ustar00rootroot00000000000000connect-go-1.13.0/.github/workflows/add-to-project.yaml000066400000000000000000000012721453471351600227400ustar00rootroot00000000000000name: Add issues and PRs to project on: issues: types: - opened - reopened - transferred pull_request_target: types: - opened - reopened jobs: add-to-project: name: Add issue to project runs-on: ubuntu-latest steps: - name: Get GitHub app token uses: actions/create-github-app-token@v1 id: app_token with: app-id: ${{ secrets.CONNECT_EXPORT_APP_ID }} private-key: ${{ secrets.CONNECT_EXPORT_APP_KEY }} - uses: actions/add-to-project@v0.5.0 with: project-url: https://github.com/orgs/connectrpc/projects/1 github-token: ${{ steps.app_token.outputs.token }} connect-go-1.13.0/.github/workflows/ci.yaml000066400000000000000000000035021453471351600205150ustar00rootroot00000000000000name: ci on: push: branches: [main] tags: ['v*'] pull_request: branches: [main] schedule: - cron: '15 22 * * *' workflow_dispatch: {} # support manual runs permissions: contents: read jobs: ci: runs-on: ubuntu-latest strategy: matrix: # when editing this list, also update steps and jobs below go-version: [1.19.x, 1.20.x, 1.21.x] steps: - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Unit Test run: make shorttest - name: Lint # Often, lint & gofmt guidelines depend on the Go version. To prevent # conflicting guidance, run only on the most recent supported version. # For the same reason, only check generated code on the most recent # supported version. if: matrix.go-version == '1.21.x' run: make checkgenerate && make lint conformance: runs-on: ubuntu-latest strategy: matrix: # 1.19 is omitted because conformance test runner requires 1.20+ go-version: [1.20.x, 1.21.x] steps: - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Run Conformance Tests run: make runconformance slowtest: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install Go uses: actions/setup-go@v4 with: # only the latest go-version: 1.21.x - name: Run Slow Tests run: make slowtestconnect-go-1.13.0/.github/workflows/pr-title.yaml000066400000000000000000000011441453471351600216620ustar00rootroot00000000000000name: Lint PR Title # Prevent writing to the repository using the CI token. # Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions permissions: pull-requests: read on: pull_request: # By default, a workflow only runs when a pull_request's activity type is opened, # synchronize, or reopened. We explicity override here so that PR titles are # re-linted when the PR text content is edited. types: - opened - edited - reopened - synchronize jobs: lint: uses: bufbuild/base-workflows/.github/workflows/pr-title.yaml@main connect-go-1.13.0/.github/workflows/windows.yaml000066400000000000000000000012441453471351600216150ustar00rootroot00000000000000name: windows on: push: branches: [main] tags: ['v*'] pull_request: branches: [main] schedule: - cron: '15 22 * * *' workflow_dispatch: {} # support manual runs permissions: contents: read jobs: ci: runs-on: windows-latest strategy: matrix: go-version: [1.19.x,1.20.x,1.21.x] steps: - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Test shell: bash run: | go build ./... go test -vet=off -race ./... connect-go-1.13.0/.gitignore000066400000000000000000000000541453471351600156300ustar00rootroot00000000000000/.tmp/ *.pprof *.svg cover.out connect.test connect-go-1.13.0/.golangci.yml000066400000000000000000000123551453471351600162330ustar00rootroot00000000000000run: skip-dirs-use-default: false 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 alias: connect - 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 - deadcode # abandoned - exhaustivestruct # replaced by exhaustruct - funlen # rely on code review to limit function length - gocognit # dubious "cognitive overhead" quantification - gofumpt # prefer standard gofmt - goimports # rely on gci instead - golint # deprecated by Go team - gomnd # some unnamed constants are okay - ifshort # deprecated by author - inamedparam # convention is not followed - interfacer # deprecated by author - ireturn # "accept interfaces, return structs" isn't ironclad - lll # don't want hard limits for line length - maintidx # covered by gocyclo - maligned # readability trumps efficient struct packing - nlreturn # generous whitespace violates house style - nonamedreturns # named returns are fine; it's *bare* returns that are bad - nosnakecase # deprecated in https://github.com/golangci/golangci-lint/pull/3065 - protogetter # too many false positives - scopelint # deprecated by author - structcheck # abandoned - testpackage # internal tests are fine - varcheck # abandoned - wrapcheck # don't _always_ need to wrap errors - wsl # generous whitespace violates house style issues: exclude: # Don't ban use of fmt.Errorf to create new errors, but the remaining # checks from err113 are useful. - "err113: do not define dynamic errors.*" 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 need to initialize default grpc User-Agent - linters: [gochecknoglobals] path: protocol_grpc.go # We need to initialize default connect User-Agent - linters: [gochecknoglobals] path: protocol_connect.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 connect-go-1.13.0/LICENSE000066400000000000000000000261321453471351600146520ustar00rootroot00000000000000 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-2023 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. connect-go-1.13.0/MAINTAINERS.md000066400000000000000000000006171453471351600157410ustar00rootroot00000000000000Maintainers =========== ## Current * [Peter Edge](https://github.com/bufdev), [Buf](https://buf.build) * [Akshay Shah](https://github.com/akshayjshah), [Buf](https://buf.build) * [Josh Humphries](https://github.com/jhump), [Buf](https://buf.build) * [Matt Robenolt](https://github.com/mattrobenolt), [PlanetScale](https://planetscale.com) ## Former * [Alex McKinney](https://github.com/amckinney) connect-go-1.13.0/Makefile000066400000000000000000000066551453471351600153150ustar00rootroot00000000000000# 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 := $(BIN):$(PATH) export GOBIN := $(abspath $(BIN)) COPYRIGHT_YEARS := 2021-2023 LICENSE_IGNORE := --ignore /testdata/ .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 rm -rf internal/gen PATH="$(abspath $(BIN))" 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@v1.27.2 $(BIN)/license-header: Makefile @mkdir -p $(@D) go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v1.27.2 $(BIN)/golangci-lint: Makefile @mkdir -p $(@D) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 $(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 connect-go-1.13.0/README.md000066400000000000000000000153611453471351600151260ustar00rootroot00000000000000Connect ======= [![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] 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/connect/ping/v1/pingv1connect/ping.connect.go). Using that package, we can build a server: ```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/connect/ping/v1/pingv1connect" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) type PingServer struct { pingv1connect.UnimplementedPingServiceHandler // returns errors from all methods } func (ps *PingServer) Ping( ctx context.Context, req *connect.Request[pingv1.PingRequest], ) (*connect.Response[pingv1.PingResponse], error) { // connect.Request and connect.Response give you direct access to headers and // trailers. No context-based nonsense! log.Println(req.Header().Get("Some-Header")) res := connect.NewResponse(&pingv1.PingResponse{ // req.Msg is a strongly-typed *pingv1.PingRequest, so we can access its // fields without type assertions. Number: req.Msg.Number, }) res.Header().Set("Some-Other-Header", "hello!") return res, nil } func main() { mux := http.NewServeMux() // The generated constructors return a path and a plain net/http // handler. mux.Handle(pingv1connect.NewPingServiceHandler(&PingServer{})) err := http.ListenAndServe( "localhost:8080", // For gRPC clients, it's convenient to support HTTP/2 without TLS. You can // avoid x/net/http2 by using http.ListenAndServeTLS. h2c.NewHandler(mux, &http2.Server{}), ) 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" "connectrpc.com/connect" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" ) func main() { client := pingv1connect.NewPingServiceClient( http.DefaultClient, "http://localhost:8080/", ) req := connect.NewRequest(&pingv1.PingRequest{ Number: 42, }) req.Header().Set("Some-Header", "hello from connect") res, err := client.Ping(context.Background(), req) if err != nil { log.Fatalln(err) } log.Println(res.Msg) log.Println(res.Header().Get("Some-Other-Header")) } ``` 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 * [grpcreflect]: gRPC-compatible server reflection * [examples-go]: service powering 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 three most recent major releases of Go. Keep in mind that [only the last two releases 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 connect-go-1.13.0/SECURITY.md000066400000000000000000000002341453471351600154310ustar00rootroot00000000000000Security Policy =============== This project follows the [Connect security policy and reporting process](https://connectrpc.com/docs/governance/security). connect-go-1.13.0/bench_test.go000066400000000000000000000167451453471351600163230ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "bytes" "compress/gzip" "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" connect "connectrpc.com/connect" "connectrpc.com/connect/internal/assert" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" ) func BenchmarkConnect(b *testing.B) { mux := http.NewServeMux() mux.Handle( pingv1connect.NewPingServiceHandler( &ExamplePingServer{}, ), ) server := httptest.NewUnstartedServer(mux) server.EnableHTTP2 = true server.StartTLS() b.Cleanup(server.Close) httpClient := server.Client() httpTransport, ok := httpClient.Transport.(*http.Transport) assert.True(b, ok) httpTransport.DisableCompression = true clients := []struct { name string opts []connect.ClientOption }{{ name: "connect", opts: []connect.ClientOption{}, }, { name: "grpc", opts: []connect.ClientOption{ connect.WithGRPC(), }, }, { name: "grpcweb", opts: []connect.ClientOption{ connect.WithGRPCWeb(), }, }} twoMiB := strings.Repeat("a", 2*1024*1024) for _, client := range clients { b.Run(client.name, func(b *testing.B) { client := pingv1connect.NewPingServiceClient( httpClient, server.URL, connect.WithSendGzip(), connect.WithClientOptions(client.opts...), ) ctx := context.Background() b.Run("unary_big", func(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { if _, err := client.Ping( ctx, connect.NewRequest(&pingv1.PingRequest{Text: twoMiB}), ); err != nil { b.Error(err) } } }) }) b.Run("unary_small", func(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { response, err := client.Ping( ctx, connect.NewRequest(&pingv1.PingRequest{Number: 42}), ) if err != nil { b.Error(err) } else if num := response.Msg.GetNumber(); num != 42 { b.Errorf("expected 42, got %d", num) } } }) }) b.Run("client_stream", func(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { const ( upTo = 1 expect = 1 ) stream := client.Sum(ctx) for number := int64(1); number <= upTo; number++ { if err := stream.Send(&pingv1.SumRequest{Number: number}); err != nil { b.Error(err) } } response, err := stream.CloseAndReceive() if err != nil { b.Error(err) } else if got := response.Msg.GetSum(); got != expect { b.Errorf("expected %d, got %d", expect, got) } } }) }) b.Run("server_stream", func(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { const ( upTo = 1 ) request := connect.NewRequest(&pingv1.CountUpRequest{Number: upTo}) stream, err := client.CountUp(ctx, request) if err != nil { b.Error(err) return } number := int64(1) for ; stream.Receive(); number++ { if got := stream.Msg().GetNumber(); got != number { b.Errorf("expected %d, got %d", number, got) } } if number != upTo+1 { b.Errorf("expected %d, got %d", upTo+1, number) } } }) }) b.Run("bidi_stream", func(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { const ( upTo = 1 ) stream := client.CumSum(ctx) number := int64(1) for ; number <= upTo; number++ { if err := stream.Send(&pingv1.CumSumRequest{Number: number}); err != nil { b.Error(err) } msg, err := stream.Receive() if err != nil { b.Error(err) } if got, expected := msg.GetSum(), number*(number+1)/2; got != expected { b.Errorf("expected %d, got %d", expected, got) } } if err := stream.CloseRequest(); err != nil { b.Error(err) } if err := stream.CloseResponse(); err != nil { b.Error(err) } } }) }) }) } } type ping struct { Text string `json:"text"` } func BenchmarkREST(b *testing.B) { handler := func(writer http.ResponseWriter, request *http.Request) { defer request.Body.Close() defer func() { _, err := io.Copy(io.Discard, request.Body) assert.Nil(b, err) }() writer.Header().Set("Content-Type", "application/json") var body io.Reader = request.Body if request.Header.Get("Content-Encoding") == "gzip" { gzipReader, err := gzip.NewReader(body) if err != nil { b.Fatalf("get gzip reader: %v", err) } defer gzipReader.Close() body = gzipReader } var out io.Writer = writer if strings.Contains(request.Header.Get("Accept-Encoding"), "gzip") { writer.Header().Set("Content-Encoding", "gzip") gzipWriter := gzip.NewWriter(writer) defer gzipWriter.Close() out = gzipWriter } raw, err := io.ReadAll(body) if err != nil { b.Fatalf("read body: %v", err) } var pingRequest ping if err := json.Unmarshal(raw, &pingRequest); err != nil { b.Fatalf("json unmarshal: %v", err) } bs, err := json.Marshal(&pingRequest) if err != nil { b.Fatalf("json marshal: %v", err) } _, err = out.Write(bs) assert.Nil(b, err) } server := httptest.NewUnstartedServer(http.HandlerFunc(handler)) server.EnableHTTP2 = true server.StartTLS() b.Cleanup(server.Close) twoMiB := strings.Repeat("a", 2*1024*1024) b.ResetTimer() b.Run("unary", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { unaryRESTIteration(b, server.Client(), server.URL, twoMiB) } }) }) } func unaryRESTIteration(b *testing.B, client *http.Client, url string, text string) { b.Helper() rawRequestBody := bytes.NewBuffer(nil) compressedRequestBody := gzip.NewWriter(rawRequestBody) encoder := json.NewEncoder(compressedRequestBody) if err := encoder.Encode(&ping{text}); err != nil { b.Fatalf("marshal request: %v", err) } compressedRequestBody.Close() request, err := http.NewRequestWithContext( context.Background(), http.MethodPost, url, rawRequestBody, ) if err != nil { b.Fatalf("construct request: %v", err) } request.Header.Set("Content-Encoding", "gzip") request.Header.Set("Accept-Encoding", "gzip") request.Header.Set("Content-Type", "application/json") response, err := client.Do(request) if err != nil { b.Fatalf("do request: %v", err) } defer func() { _, err := io.Copy(io.Discard, response.Body) assert.Nil(b, err) }() if response.StatusCode != http.StatusOK { b.Fatalf("response status: %v", response.Status) } uncompressed, err := gzip.NewReader(response.Body) if err != nil { b.Fatalf("uncompress response: %v", err) } raw, err := io.ReadAll(uncompressed) if err != nil { b.Fatalf("read response: %v", err) } var got ping if err := json.Unmarshal(raw, &got); err != nil { b.Fatalf("unmarshal: %v", err) } } connect-go-1.13.0/buf.gen.yaml000066400000000000000000000004001453471351600160430ustar00rootroot00000000000000version: v1 managed: enabled: true go_package_prefix: default: connectrpc.com/connect/internal/gen plugins: - name: go out: internal/gen opt: paths=source_relative - name: connect-go out: internal/gen opt: paths=source_relative connect-go-1.13.0/buf.work.yaml000066400000000000000000000000541453471351600162610ustar00rootroot00000000000000version: v1 directories: - internal/proto connect-go-1.13.0/buffer_pool.go000066400000000000000000000024161453471351600164750ustar00rootroot00000000000000// Copyright 2021-2023 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) } connect-go-1.13.0/client.go000066400000000000000000000220471453471351600154530ustar00rootroot00000000000000// Copyright 2021-2023 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) }) // 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 { 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()) 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. 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, } } // 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.spec = conn.Spec() request.peer = conn.Peer() 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. 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, } } func (c *Client[Req, Res]) newConn(ctx context.Context, streamType StreamType, onRequestSend func(r *http.Request)) StreamingClientConn { 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) } return newConn(ctx, c.config.newSpec(streamType)) } 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) } connect-go-1.13.0/client_example_test.go000066400000000000000000000033341453471351600202230ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "context" "log" "net/http" "os" connect "connectrpc.com/connect" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" ) func Example_client() { logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */) // To keep this example runnable, we'll use an HTTP server and client // that communicate over in-memory pipes. The client is still a plain // *http.Client! var httpClient *http.Client = examplePingServer.Client() // By default, clients use the Connect protocol. Add connect.WithGRPC() or // connect.WithGRPCWeb() to switch protocols. client := pingv1connect.NewPingServiceClient( httpClient, examplePingServer.URL(), ) response, err := client.Ping( context.Background(), connect.NewRequest(&pingv1.PingRequest{Number: 42}), ) if err != nil { logger.Println("error:", err) return } logger.Println("response content-type:", response.Header().Get("Content-Type")) logger.Println("response message:", response.Msg) // Output: // response content-type: application/proto // response message: number:42 } connect-go-1.13.0/client_ext_test.go000066400000000000000000000611661453471351600173770ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "bytes" "context" "crypto/rand" "errors" "fmt" "io" "net/http" "net/http/httptest" "runtime" "strings" "sync" "sync/atomic" "testing" "time" connect "connectrpc.com/connect" "connectrpc.com/connect/internal/assert" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" "connectrpc.com/connect/internal/memhttp/memhttptest" "google.golang.org/protobuf/encoding/protowire" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/dynamicpb" ) func TestNewClient_InitFailure(t *testing.T) { t.Parallel() client := pingv1connect.NewPingServiceClient( http.DefaultClient, "http://127.0.0.1:8080", // This triggers an error during initialization, so each call will short circuit returning an error. connect.WithSendCompression("invalid"), ) validateExpectedError := func(t *testing.T, err error) { t.Helper() assert.NotNil(t, err) var connectErr *connect.Error assert.True(t, errors.As(err, &connectErr)) assert.Equal(t, connectErr.Message(), `unknown compression "invalid"`) } t.Run("unary", func(t *testing.T) { t.Parallel() _, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{})) validateExpectedError(t, err) }) t.Run("bidi", func(t *testing.T) { t.Parallel() bidiStream := client.CumSum(context.Background()) err := bidiStream.Send(&pingv1.CumSumRequest{}) validateExpectedError(t, err) }) t.Run("client_stream", func(t *testing.T) { t.Parallel() clientStream := client.Sum(context.Background()) err := clientStream.Send(&pingv1.SumRequest{}) validateExpectedError(t, err) }) t.Run("server_stream", func(t *testing.T) { t.Parallel() _, err := client.CountUp(context.Background(), connect.NewRequest(&pingv1.CountUpRequest{Number: 3})) validateExpectedError(t, err) }) } func TestClientPeer(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) server := memhttptest.NewServer(t, mux) run := func(t *testing.T, unaryHTTPMethod string, opts ...connect.ClientOption) { t.Helper() client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), connect.WithClientOptions(opts...), connect.WithInterceptors(&assertPeerInterceptor{t}), ) ctx := context.Background() t.Run("unary", func(t *testing.T) { unaryReq := connect.NewRequest[pingv1.PingRequest](nil) _, err := client.Ping(ctx, unaryReq) assert.Nil(t, err) assert.Equal(t, unaryHTTPMethod, unaryReq.HTTPMethod()) text := strings.Repeat(".", 256) r, err := client.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{Text: text})) assert.Nil(t, err) assert.Equal(t, r.Msg.GetText(), text) }) t.Run("client_stream", func(t *testing.T) { clientStream := client.Sum(ctx) t.Cleanup(func() { _, closeErr := clientStream.CloseAndReceive() assert.Nil(t, closeErr) }) assert.NotZero(t, clientStream.Peer().Addr) assert.NotZero(t, clientStream.Peer().Protocol) err := clientStream.Send(&pingv1.SumRequest{}) assert.Nil(t, err) }) t.Run("server_stream", func(t *testing.T) { serverStream, err := client.CountUp(ctx, connect.NewRequest(&pingv1.CountUpRequest{})) t.Cleanup(func() { assert.Nil(t, serverStream.Close()) }) assert.Nil(t, err) }) t.Run("bidi_stream", func(t *testing.T) { bidiStream := client.CumSum(ctx) t.Cleanup(func() { assert.Nil(t, bidiStream.CloseRequest()) assert.Nil(t, bidiStream.CloseResponse()) }) assert.NotZero(t, bidiStream.Peer().Addr) assert.NotZero(t, bidiStream.Peer().Protocol) err := bidiStream.Send(&pingv1.CumSumRequest{}) assert.Nil(t, err) }) } t.Run("connect", func(t *testing.T) { t.Parallel() run(t, http.MethodPost) }) t.Run("connect+get", func(t *testing.T) { t.Parallel() run(t, http.MethodGet, connect.WithHTTPGet(), connect.WithSendGzip(), ) }) t.Run("grpc", func(t *testing.T) { t.Parallel() run(t, http.MethodPost, connect.WithGRPC()) }) t.Run("grpcweb", func(t *testing.T) { t.Parallel() run(t, http.MethodPost, connect.WithGRPCWeb()) }) } func TestGetNotModified(t *testing.T) { t.Parallel() const etag = "some-etag" // Handlers should automatically set Vary to include request headers that are // part of the RPC protocol. expectVary := []string{"Accept-Encoding"} mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(¬ModifiedPingServer{etag: etag})) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), connect.WithHTTPGet(), ) ctx := context.Background() // unconditional request unaryReq := connect.NewRequest(&pingv1.PingRequest{}) res, err := client.Ping(ctx, unaryReq) assert.Nil(t, err) assert.Equal(t, res.Header().Get("Etag"), etag) assert.Equal(t, res.Header().Values("Vary"), expectVary) assert.Equal(t, http.MethodGet, unaryReq.HTTPMethod()) unaryReq = connect.NewRequest(&pingv1.PingRequest{}) unaryReq.Header().Set("If-None-Match", etag) _, err = client.Ping(ctx, unaryReq) assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeUnknown) assert.True(t, connect.IsNotModifiedError(err)) var connectErr *connect.Error assert.True(t, errors.As(err, &connectErr)) assert.Equal(t, connectErr.Meta().Get("Etag"), etag) assert.Equal(t, connectErr.Meta().Values("Vary"), expectVary) assert.Equal(t, http.MethodGet, unaryReq.HTTPMethod()) } func TestGetNoContentHeaders(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(&pingServer{})) server := memhttptest.NewServer(t, http.HandlerFunc(func(respWriter http.ResponseWriter, req *http.Request) { if len(req.Header.Values("content-type")) > 0 || len(req.Header.Values("content-encoding")) > 0 || len(req.Header.Values("content-length")) > 0 { http.Error(respWriter, "GET request should not include content headers", http.StatusBadRequest) } mux.ServeHTTP(respWriter, req) })) client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), connect.WithHTTPGet(), ) ctx := context.Background() unaryReq := connect.NewRequest(&pingv1.PingRequest{}) _, err := client.Ping(ctx, unaryReq) assert.Nil(t, err) assert.Equal(t, http.MethodGet, unaryReq.HTTPMethod()) } func TestSpecSchema(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler( pingServer{}, connect.WithInterceptors(&assertSchemaInterceptor{t}), )) server := memhttptest.NewServer(t, mux) ctx := context.Background() client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), connect.WithInterceptors(&assertSchemaInterceptor{t}), ) t.Run("unary", func(t *testing.T) { t.Parallel() unaryReq := connect.NewRequest[pingv1.PingRequest](nil) _, err := client.Ping(ctx, unaryReq) assert.NotNil(t, unaryReq.Spec().Schema) assert.Nil(t, err) text := strings.Repeat(".", 256) r, err := client.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{Text: text})) assert.Nil(t, err) assert.Equal(t, r.Msg.GetText(), text) }) t.Run("bidi_stream", func(t *testing.T) { t.Parallel() bidiStream := client.CumSum(ctx) t.Cleanup(func() { assert.Nil(t, bidiStream.CloseRequest()) assert.Nil(t, bidiStream.CloseResponse()) }) assert.NotZero(t, bidiStream.Spec().Schema) err := bidiStream.Send(&pingv1.CumSumRequest{}) assert.Nil(t, err) }) } func TestDynamicClient(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) server := memhttptest.NewServer(t, mux) ctx := context.Background() initializer := func(spec connect.Spec, msg any) error { dynamic, ok := msg.(*dynamicpb.Message) if !ok { return nil } desc, ok := spec.Schema.(protoreflect.MethodDescriptor) if !ok { return fmt.Errorf("invalid schema type %T for %T message", spec.Schema, dynamic) } if spec.IsClient { *dynamic = *dynamicpb.NewMessage(desc.Output()) } else { *dynamic = *dynamicpb.NewMessage(desc.Input()) } return nil } t.Run("unary", func(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.Ping") assert.Nil(t, err) methodDesc, ok := desc.(protoreflect.MethodDescriptor) assert.True(t, ok) client := connect.NewClient[dynamicpb.Message, dynamicpb.Message]( server.Client(), server.URL()+"/connect.ping.v1.PingService/Ping", connect.WithSchema(methodDesc), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithResponseInitializer(initializer), ) msg := dynamicpb.NewMessage(methodDesc.Input()) msg.Set( methodDesc.Input().Fields().ByName("number"), protoreflect.ValueOfInt64(42), ) res, err := client.CallUnary(ctx, connect.NewRequest(msg)) assert.Nil(t, err) got := res.Msg.Get(methodDesc.Output().Fields().ByName("number")).Int() assert.Equal(t, got, 42) }) t.Run("clientStream", func(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.Sum") assert.Nil(t, err) methodDesc, ok := desc.(protoreflect.MethodDescriptor) assert.True(t, ok) client := connect.NewClient[dynamicpb.Message, dynamicpb.Message]( server.Client(), server.URL()+"/connect.ping.v1.PingService/Sum", connect.WithSchema(methodDesc), connect.WithResponseInitializer(initializer), ) stream := client.CallClientStream(ctx) msg := dynamicpb.NewMessage(methodDesc.Input()) msg.Set( methodDesc.Input().Fields().ByName("number"), protoreflect.ValueOfInt64(42), ) assert.Nil(t, stream.Send(msg)) assert.Nil(t, stream.Send(msg)) rsp, err := stream.CloseAndReceive() if !assert.Nil(t, err) { return } got := rsp.Msg.Get(methodDesc.Output().Fields().ByName("sum")).Int() assert.Equal(t, got, 42*2) }) t.Run("serverStream", func(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.CountUp") assert.Nil(t, err) methodDesc, ok := desc.(protoreflect.MethodDescriptor) assert.True(t, ok) client := connect.NewClient[dynamicpb.Message, dynamicpb.Message]( server.Client(), server.URL()+"/connect.ping.v1.PingService/CountUp", connect.WithSchema(methodDesc), connect.WithResponseInitializer(initializer), ) msg := dynamicpb.NewMessage(methodDesc.Input()) msg.Set( methodDesc.Input().Fields().ByName("number"), protoreflect.ValueOfInt64(2), ) req := connect.NewRequest(msg) stream, err := client.CallServerStream(ctx, req) if !assert.Nil(t, err) { return } for i := 1; stream.Receive(); i++ { out := stream.Msg() got := out.Get(methodDesc.Output().Fields().ByName("number")).Int() assert.Equal(t, got, int64(i)) } assert.Nil(t, stream.Close()) }) t.Run("bidi", func(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.CumSum") assert.Nil(t, err) methodDesc, ok := desc.(protoreflect.MethodDescriptor) assert.True(t, ok) client := connect.NewClient[dynamicpb.Message, dynamicpb.Message]( server.Client(), server.URL()+"/connect.ping.v1.PingService/CumSum", connect.WithSchema(methodDesc), connect.WithResponseInitializer(initializer), ) stream := client.CallBidiStream(ctx) msg := dynamicpb.NewMessage(methodDesc.Input()) msg.Set( methodDesc.Input().Fields().ByName("number"), protoreflect.ValueOfInt64(42), ) assert.Nil(t, stream.Send(msg)) assert.Nil(t, stream.CloseRequest()) out, err := stream.Receive() if assert.Nil(t, err) { return } got := out.Get(methodDesc.Output().Fields().ByName("number")).Int() assert.Equal(t, got, 42) }) t.Run("option", func(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.Ping") assert.Nil(t, err) methodDesc, ok := desc.(protoreflect.MethodDescriptor) assert.True(t, ok) optionCalled := false client := connect.NewClient[dynamicpb.Message, dynamicpb.Message]( server.Client(), server.URL()+"/connect.ping.v1.PingService/Ping", connect.WithSchema(methodDesc), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithResponseInitializer( func(spec connect.Spec, msg any) error { assert.NotNil(t, spec) assert.NotNil(t, msg) dynamic, ok := msg.(*dynamicpb.Message) if !assert.True(t, ok) { return fmt.Errorf("unexpected message type: %T", msg) } *dynamic = *dynamicpb.NewMessage(methodDesc.Output()) optionCalled = true return nil }, ), ) msg := dynamicpb.NewMessage(methodDesc.Input()) msg.Set( methodDesc.Input().Fields().ByName("number"), protoreflect.ValueOfInt64(42), ) res, err := client.CallUnary(ctx, connect.NewRequest(msg)) assert.Nil(t, err) got := res.Msg.Get(methodDesc.Output().Fields().ByName("number")).Int() assert.Equal(t, got, 42) assert.True(t, optionCalled) }) } func TestClientDeadlineHandling(t *testing.T) { t.Parallel() if testing.Short() { t.Skip("skipping slow test") } // Note that these tests are not able to reproduce issues with the race // detector enabled. That's partly why the makefile only runs "slow" // tests with the race detector disabled. _, handler := pingv1connect.NewPingServiceHandler(pingServer{}) svr := httptest.NewUnstartedServer(http.HandlerFunc(func(respWriter http.ResponseWriter, req *http.Request) { if req.Context().Err() != nil { return } handler.ServeHTTP(respWriter, req) })) svr.EnableHTTP2 = true svr.StartTLS() t.Cleanup(svr.Close) // This case creates a new connection for each RPC to verify that timeouts during dialing // won't cause issues. This is historically easier to reproduce, so it uses a smaller // duration, no concurrency, and fewer iterations. This is important because if we used // a new connection for each RPC in the bigger test scenario below, we'd encounter other // issues related to overwhelming the loopback interface and exhausting ephemeral ports. t.Run("dial", func(t *testing.T) { t.Parallel() transport, ok := svr.Client().Transport.(*http.Transport) if !assert.True(t, ok) { t.FailNow() } testClientDeadlineBruteForceLoop(t, 5*time.Second, 5, 1, func(ctx context.Context) (string, rpcErrors) { httpClient := &http.Client{ Transport: transport.Clone(), } client := pingv1connect.NewPingServiceClient(httpClient, svr.URL) _, err := client.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{Text: "foo"})) // Close all connections and make sure to give a little time for the OS to // release socket resources to prevent resource exhaustion (such as running // out of ephemeral ports). httpClient.CloseIdleConnections() time.Sleep(time.Millisecond / 2) return pingv1connect.PingServicePingProcedure, rpcErrors{recvErr: err} }, ) }) // This case creates significantly more load than the above one, but uses a normal // client so pools and re-uses connections. It also uses all stream types to send // messages, to make sure that all stream implementations handle deadlines correctly. // The I/O errors related to deadlines are historically harder to reproduce, so it // throws a lot more effort into reproducing, particularly a longer duration for // which it will run. It also uses larger messages (by packing requests with // unrecognized fields) and compression, to make it more likely to encounter the // deadline in the middle of read and write operations. t.Run("read-write", func(t *testing.T) { t.Parallel() var extraField []byte extraField = protowire.AppendTag(extraField, 999, protowire.BytesType) extraData := make([]byte, 16*1024) // use good random data so it's not very compressible if _, err := rand.Read(extraData); err != nil { t.Fatalf("failed to generate extra payload: %v", err) return } extraField = protowire.AppendBytes(extraField, extraData) clientConnect := pingv1connect.NewPingServiceClient(svr.Client(), svr.URL, connect.WithSendGzip()) clientGRPC := pingv1connect.NewPingServiceClient(svr.Client(), svr.URL, connect.WithSendGzip(), connect.WithGRPCWeb()) var count atomic.Int32 testClientDeadlineBruteForceLoop(t, 20*time.Second, 200, runtime.GOMAXPROCS(0), func(ctx context.Context) (string, rpcErrors) { var procedure string var errs rpcErrors rpcNum := count.Add(1) var client pingv1connect.PingServiceClient if rpcNum&4 == 0 { client = clientConnect } else { client = clientGRPC } switch rpcNum & 3 { case 0: procedure = pingv1connect.PingServicePingProcedure _, errs.recvErr = client.Ping(ctx, connect.NewRequest(addUnrecognizedBytes(&pingv1.PingRequest{Text: "foo"}, extraField))) case 1: procedure = pingv1connect.PingServiceSumProcedure stream := client.Sum(ctx) for i := 0; i < 3; i++ { errs.sendErr = stream.Send(addUnrecognizedBytes(&pingv1.SumRequest{Number: 1}, extraField)) if errs.sendErr != nil { break } } _, errs.recvErr = stream.CloseAndReceive() case 2: procedure = pingv1connect.PingServiceCountUpProcedure var stream *connect.ServerStreamForClient[pingv1.CountUpResponse] stream, errs.recvErr = client.CountUp(ctx, connect.NewRequest(addUnrecognizedBytes(&pingv1.CountUpRequest{Number: 3}, extraField))) if errs.recvErr == nil { for stream.Receive() { } errs.recvErr = stream.Err() errs.closeRecvErr = stream.Close() } case 3: procedure = pingv1connect.PingServiceCumSumProcedure stream := client.CumSum(ctx) for i := 0; i < 3; i++ { errs.sendErr = stream.Send(addUnrecognizedBytes(&pingv1.CumSumRequest{Number: 1}, extraField)) _, errs.recvErr = stream.Receive() if errs.recvErr != nil { break } } errs.closeSendErr = stream.CloseRequest() errs.closeRecvErr = stream.CloseResponse() } return procedure, errs }, ) }) } func testClientDeadlineBruteForceLoop( t *testing.T, duration time.Duration, iterationsPerDeadline int, parallelism int, loopBody func(ctx context.Context) (string, rpcErrors), ) { t.Helper() testContext, testCancel := context.WithTimeout(context.Background(), duration) defer testCancel() var rpcCount atomic.Int64 var wg sync.WaitGroup for goroutine := 0; goroutine < parallelism; goroutine++ { goroutine := goroutine wg.Add(1) go func() { defer wg.Done() // We try a range of timeouts since the timing issue is sensitive // to execution environment (e.g. CPU, memory, and network speeds). // So the lower timeout values may be more likely to trigger an issue // in faster environments; higher timeouts for slower environments. const minTimeout = 10 * time.Microsecond const maxTimeout = 2 * time.Millisecond for { for timeout := minTimeout; timeout <= maxTimeout; timeout += 10 * time.Microsecond { for i := 0; i < iterationsPerDeadline; i++ { if testContext.Err() != nil { return } ctx, cancel := context.WithTimeout(context.Background(), timeout) // We are intentionally not inheriting from testContext, which signals when the // test loop should stop and return but need not influence the RPC deadline. proc, errs := loopBody(ctx) //nolint:contextcheck rpcCount.Add(1) cancel() type errCase struct { err error name string allowEOF bool } errCases := []errCase{ { err: errs.sendErr, name: "send error", allowEOF: true, }, { err: errs.recvErr, name: "receive error", }, { err: errs.closeSendErr, name: "close-send error", }, { err: errs.closeRecvErr, name: "close-receive error", }, } for _, errCase := range errCases { err := errCase.err if err == nil { // operation completed before timeout, try again continue } if errCase.allowEOF && errors.Is(err, io.EOF) { continue } if !assert.Equal(t, connect.CodeOf(err), connect.CodeDeadlineExceeded) { var buf bytes.Buffer _, _ = fmt.Fprintf(&buf, "actual %v from %s: %v\n%#v", errCase.name, proc, err, err) for { err = errors.Unwrap(err) if err == nil { break } _, _ = fmt.Fprintf(&buf, "\n caused by: %#v", err) } t.Log(buf.String()) testCancel() } } } } t.Logf("goroutine %d: repeating duration loop", goroutine) } }() } wg.Wait() t.Logf("Issued %d RPCs.", rpcCount.Load()) } type notModifiedPingServer struct { pingv1connect.UnimplementedPingServiceHandler etag string } func (s *notModifiedPingServer) Ping( _ context.Context, req *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { if req.HTTPMethod() == http.MethodGet && req.Header().Get("If-None-Match") == s.etag { return nil, connect.NewNotModifiedError(http.Header{"Etag": []string{s.etag}}) } resp := connect.NewResponse(&pingv1.PingResponse{}) resp.Header().Set("Etag", s.etag) return resp, nil } type assertPeerInterceptor struct { tb testing.TB } func (a *assertPeerInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { assert.NotZero(a.tb, req.Peer().Addr) assert.NotZero(a.tb, req.Peer().Protocol) return next(ctx, req) } } func (a *assertPeerInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { conn := next(ctx, spec) assert.NotZero(a.tb, conn.Peer().Addr) assert.NotZero(a.tb, conn.Peer().Protocol) assert.NotZero(a.tb, conn.Spec()) return conn } } func (a *assertPeerInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return func(ctx context.Context, conn connect.StreamingHandlerConn) error { assert.NotZero(a.tb, conn.Peer().Addr) assert.NotZero(a.tb, conn.Peer().Protocol) assert.NotZero(a.tb, conn.Spec()) return next(ctx, conn) } } type assertSchemaInterceptor struct { tb testing.TB } func (a *assertSchemaInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { if !assert.NotNil(a.tb, req.Spec().Schema) { return next(ctx, req) } methodDesc, ok := req.Spec().Schema.(protoreflect.MethodDescriptor) if assert.True(a.tb, ok) { procedure := fmt.Sprintf("/%s/%s", methodDesc.Parent().FullName(), methodDesc.Name()) assert.Equal(a.tb, procedure, req.Spec().Procedure) } return next(ctx, req) } } func (a *assertSchemaInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { conn := next(ctx, spec) if !assert.NotNil(a.tb, spec.Schema) { return conn } methodDescriptor, ok := spec.Schema.(protoreflect.MethodDescriptor) if assert.True(a.tb, ok) { procedure := fmt.Sprintf("/%s/%s", methodDescriptor.Parent().FullName(), methodDescriptor.Name()) assert.Equal(a.tb, procedure, spec.Procedure) } return conn } } func (a *assertSchemaInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return func(ctx context.Context, conn connect.StreamingHandlerConn) error { if !assert.NotNil(a.tb, conn.Spec().Schema) { return next(ctx, conn) } methodDesc, ok := conn.Spec().Schema.(protoreflect.MethodDescriptor) if assert.True(a.tb, ok) { procedure := fmt.Sprintf("/%s/%s", methodDesc.Parent().FullName(), methodDesc.Name()) assert.Equal(a.tb, procedure, conn.Spec().Procedure) } return next(ctx, conn) } } type rpcErrors struct { sendErr error recvErr error closeSendErr error closeRecvErr error } func addUnrecognizedBytes[M proto.Message](msg M, data []byte) M { msg.ProtoReflect().SetUnknown(data) return msg } connect-go-1.13.0/client_get_fallback_test.go000066400000000000000000000034651453471351600211730ustar00rootroot00000000000000// Copyright 2021-2023 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" "strings" "testing" "connectrpc.com/connect/internal/assert" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/memhttp/memhttptest" ) func TestClientUnaryGetFallback(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle("/connect.ping.v1.PingService/Ping", NewUnaryHandler( "/connect.ping.v1.PingService/Ping", func(ctx context.Context, r *Request[pingv1.PingRequest]) (*Response[pingv1.PingResponse], error) { return NewResponse(&pingv1.PingResponse{ Number: r.Msg.GetNumber(), Text: r.Msg.GetText(), }), nil }, WithIdempotency(IdempotencyNoSideEffects), )) server := memhttptest.NewServer(t, mux) client := NewClient[pingv1.PingRequest, pingv1.PingResponse]( server.Client(), server.URL()+"/connect.ping.v1.PingService/Ping", WithHTTPGet(), WithHTTPGetMaxURLSize(1, true), WithSendGzip(), ) ctx := context.Background() _, err := client.CallUnary(ctx, NewRequest[pingv1.PingRequest](nil)) assert.Nil(t, err) text := strings.Repeat(".", 256) r, err := client.CallUnary(ctx, NewRequest(&pingv1.PingRequest{Text: text})) assert.Nil(t, err) assert.Equal(t, r.Msg.GetText(), text) } connect-go-1.13.0/client_stream.go000066400000000000000000000207621453471351600170300ustar00rootroot00000000000000// Copyright 2021-2023 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" ) // 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. 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 } // 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. 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. 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 } connect-go-1.13.0/client_stream_test.go000066400000000000000000000070051453471351600200620ustar00rootroot00000000000000// Copyright 2021-2023 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" "fmt" "net/http" "testing" "connectrpc.com/connect/internal/assert" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" ) func TestClientStreamForClient_NoPanics(t *testing.T) { t.Parallel() initErr := errors.New("client init failure") clientStream := &ClientStreamForClient[pingv1.PingRequest, pingv1.PingResponse]{err: initErr} assert.ErrorIs(t, clientStream.Send(&pingv1.PingRequest{}), initErr) verifyHeaders(t, clientStream.RequestHeader()) res, err := clientStream.CloseAndReceive() assert.Nil(t, res) assert.ErrorIs(t, err, initErr) conn, err := clientStream.Conn() assert.NotNil(t, err) assert.Nil(t, conn) } func TestServerStreamForClient_NoPanics(t *testing.T) { t.Parallel() initErr := errors.New("client init failure") serverStream := &ServerStreamForClient[pingv1.PingResponse]{constructErr: initErr} assert.ErrorIs(t, serverStream.Err(), initErr) assert.ErrorIs(t, serverStream.Close(), initErr) assert.NotNil(t, serverStream.Msg()) assert.False(t, serverStream.Receive()) verifyHeaders(t, serverStream.ResponseHeader()) verifyHeaders(t, serverStream.ResponseTrailer()) conn, err := serverStream.Conn() assert.NotNil(t, err) assert.Nil(t, conn) } func TestServerStreamForClient(t *testing.T) { t.Parallel() stream := &ServerStreamForClient[pingv1.PingResponse]{ conn: &nopStreamingClientConn{}, } // Ensure that each call to Receive allocates a new message. This helps // vtprotobuf, which doesn't automatically zero messages before unmarshaling // (see https://connectrpc.com/connect/issues/345), and it's also // less error-prone for users. assert.True(t, stream.Receive()) first := fmt.Sprintf("%p", stream.Msg()) assert.True(t, stream.Receive()) second := fmt.Sprintf("%p", stream.Msg()) assert.NotEqual(t, first, second) conn, err := stream.Conn() assert.Nil(t, err) assert.NotNil(t, conn) } func TestBidiStreamForClient_NoPanics(t *testing.T) { t.Parallel() initErr := errors.New("client init failure") bidiStream := &BidiStreamForClient[pingv1.CumSumRequest, pingv1.CumSumResponse]{err: initErr} res, err := bidiStream.Receive() assert.Nil(t, res) assert.ErrorIs(t, err, initErr) verifyHeaders(t, bidiStream.RequestHeader()) verifyHeaders(t, bidiStream.ResponseHeader()) verifyHeaders(t, bidiStream.ResponseTrailer()) assert.ErrorIs(t, bidiStream.Send(&pingv1.CumSumRequest{}), initErr) assert.ErrorIs(t, bidiStream.CloseRequest(), initErr) assert.ErrorIs(t, bidiStream.CloseResponse(), initErr) conn, err := bidiStream.Conn() assert.NotNil(t, err) assert.Nil(t, conn) } func verifyHeaders(t *testing.T, headers http.Header) { t.Helper() assert.Equal(t, headers, http.Header{}) // Verify set/del don't panic headers.Set("a", "b") headers.Del("a") } type nopStreamingClientConn struct { StreamingClientConn } func (c *nopStreamingClientConn) Receive(msg any) error { return nil } func (c *nopStreamingClientConn) Spec() Spec { return Spec{} } connect-go-1.13.0/cmd/000077500000000000000000000000001453471351600144045ustar00rootroot00000000000000connect-go-1.13.0/cmd/protoc-gen-connect-go/000077500000000000000000000000001453471351600205135ustar00rootroot00000000000000connect-go-1.13.0/cmd/protoc-gen-connect-go/main.go000066400000000000000000000561621453471351600220000ustar00rootroot00000000000000// Copyright 2021-2023 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. // protoc-gen-connect-go is a plugin for the Protobuf compiler that generates // Go code. To use it, build this program and make it available on your PATH as // protoc-gen-connect-go. // // The 'connect-go' suffix becomes part of the arguments for the Protobuf // compiler. To generate the base Go types and Connect code using protoc: // // protoc --go_out=gen --connect-go_out=gen path/to/file.proto // // With [buf], your buf.gen.yaml will look like this: // // version: v1 // plugins: // - name: go // out: gen // - name: connect-go // out: gen // // This generates service definitions for the Protobuf types and services // defined by file.proto. If file.proto defines the foov1 Protobuf package, the // invocations above will write output to: // // gen/path/to/file.pb.go // gen/path/to/connectfoov1/file.connect.go // // [buf]: https://buf.build package main import ( "bytes" "fmt" "os" "path" "path/filepath" "strings" "unicode/utf8" connect "connectrpc.com/connect" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/descriptorpb" "google.golang.org/protobuf/types/pluginpb" ) const ( contextPackage = protogen.GoImportPath("context") errorsPackage = protogen.GoImportPath("errors") httpPackage = protogen.GoImportPath("net/http") stringsPackage = protogen.GoImportPath("strings") connectPackage = protogen.GoImportPath("connectrpc.com/connect") generatedFilenameExtension = ".connect.go" generatedPackageSuffix = "connect" usage = "See https://connectrpc.com/docs/go/getting-started to learn how to use this plugin.\n\nFlags:\n -h, --help\tPrint this help and exit.\n --version\tPrint the version and exit." commentWidth = 97 // leave room for "// " // To propagate top-level comments, we need the field number of the syntax // declaration and the package name in the file descriptor. protoSyntaxFieldNum = 12 protoPackageFieldNum = 2 ) func main() { if len(os.Args) == 2 && os.Args[1] == "--version" { fmt.Fprintln(os.Stdout, connect.Version) os.Exit(0) } if len(os.Args) == 2 && (os.Args[1] == "-h" || os.Args[1] == "--help") { fmt.Fprintln(os.Stdout, usage) os.Exit(0) } if len(os.Args) != 1 { fmt.Fprintln(os.Stderr, usage) os.Exit(1) } protogen.Options{}.Run( func(plugin *protogen.Plugin) error { plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) for _, file := range plugin.Files { if file.Generate { generate(plugin, file) } } return nil }, ) } func generate(plugin *protogen.Plugin, file *protogen.File) { if len(file.Services) == 0 { return } file.GoPackageName += generatedPackageSuffix generatedFilenamePrefixToSlash := filepath.ToSlash(file.GeneratedFilenamePrefix) file.GeneratedFilenamePrefix = path.Join( path.Dir(generatedFilenamePrefixToSlash), string(file.GoPackageName), path.Base(generatedFilenamePrefixToSlash), ) generatedFile := plugin.NewGeneratedFile( file.GeneratedFilenamePrefix+generatedFilenameExtension, protogen.GoImportPath(path.Join( string(file.GoImportPath), string(file.GoPackageName), )), ) generatedFile.Import(file.GoImportPath) generatePreamble(generatedFile, file) generateServiceNameConstants(generatedFile, file.Services) generateServiceNameVariables(generatedFile, file) for _, service := range file.Services { generateService(generatedFile, service) } } func generatePreamble(g *protogen.GeneratedFile, file *protogen.File) { syntaxPath := protoreflect.SourcePath{protoSyntaxFieldNum} syntaxLocation := file.Desc.SourceLocations().ByPath(syntaxPath) for _, comment := range syntaxLocation.LeadingDetachedComments { leadingComments(g, protogen.Comments(comment), false /* deprecated */) } g.P() leadingComments(g, protogen.Comments(syntaxLocation.LeadingComments), false /* deprecated */) g.P() programName := filepath.Base(os.Args[0]) // Remove .exe suffix on Windows so that generated code is stable, regardless // of whether it was generated on a Windows machine or not. if ext := filepath.Ext(programName); strings.ToLower(ext) == ".exe" { programName = strings.TrimSuffix(programName, ext) } g.P("// Code generated by ", programName, ". DO NOT EDIT.") g.P("//") if file.Proto.GetOptions().GetDeprecated() { wrapComments(g, file.Desc.Path(), " is a deprecated file.") } else { g.P("// Source: ", file.Desc.Path()) } g.P() pkgPath := protoreflect.SourcePath{protoPackageFieldNum} pkgLocation := file.Desc.SourceLocations().ByPath(pkgPath) for _, comment := range pkgLocation.LeadingDetachedComments { leadingComments(g, protogen.Comments(comment), false /* deprecated */) } g.P() leadingComments(g, protogen.Comments(pkgLocation.LeadingComments), false /* deprecated */) g.P("package ", file.GoPackageName) g.P() wrapComments(g, "This is a compile-time assertion to ensure that this generated file ", "and the connect package are compatible. If you get a compiler error that this constant ", "is not defined, this code was generated with a version of connect newer than the one ", "compiled into your binary. You can fix the problem by either regenerating this code ", "with an older version of connect or updating the connect version compiled into your binary.") g.P("const _ = ", connectPackage.Ident("IsAtLeastVersion1_13_0")) g.P() } func generateServiceNameConstants(g *protogen.GeneratedFile, services []*protogen.Service) { var numMethods int g.P("const (") for _, service := range services { constName := fmt.Sprintf("%sName", service.Desc.Name()) wrapComments(g, constName, " is the fully-qualified name of the ", service.Desc.Name(), " service.") g.P(constName, ` = "`, service.Desc.FullName(), `"`) numMethods += len(service.Methods) } g.P(")") g.P() if numMethods == 0 { return } wrapComments(g, "These constants are the fully-qualified names of the RPCs defined in this package. ", "They're exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.") g.P("//") wrapComments(g, "Note that these are different from the fully-qualified method names used by ", "google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to ", "reflection-formatted method names, remove the leading slash and convert the ", "remaining slash to a period.") g.P("const (") for _, service := range services { for _, method := range service.Methods { // The runtime exposes this value as Spec.Procedure, so we should use the // same term here. wrapComments(g, procedureConstName(method), " is the fully-qualified name of the ", service.Desc.Name(), "'s ", method.Desc.Name(), " RPC.") g.P(procedureConstName(method), ` = "`, fmt.Sprintf("/%s/%s", service.Desc.FullName(), method.Desc.Name()), `"`) } } g.P(")") g.P() } func generateServiceNameVariables(g *protogen.GeneratedFile, file *protogen.File) { wrapComments(g, "These variables are the protoreflect.Descriptor objects for the RPCs defined in this package.") g.P("var (") for _, service := range file.Services { serviceDescName := unexport(fmt.Sprintf("%sServiceDescriptor", service.Desc.Name())) g.P(serviceDescName, ` = `, g.QualifiedGoIdent(file.GoDescriptorIdent), `.Services().ByName("`, service.Desc.Name(), `")`) for _, method := range service.Methods { g.P(procedureVarMethodDescriptor(method), ` = `, serviceDescName, `.Methods().ByName("`, method.Desc.Name(), `")`) } } g.P(")") } func generateService(g *protogen.GeneratedFile, service *protogen.Service) { names := newNames(service) generateClientInterface(g, service, names) generateClientImplementation(g, service, names) generateServerInterface(g, service, names) generateServerConstructor(g, service, names) generateUnimplementedServerImplementation(g, service, names) } func generateClientInterface(g *protogen.GeneratedFile, service *protogen.Service, names names) { wrapComments(g, names.Client, " is a client for the ", service.Desc.FullName(), " service.") if isDeprecatedService(service) { g.P("//") deprecated(g) } g.AnnotateSymbol(names.Client, protogen.Annotation{Location: service.Location}) g.P("type ", names.Client, " interface {") for _, method := range service.Methods { g.AnnotateSymbol(names.Client+"."+method.GoName, protogen.Annotation{Location: method.Location}) leadingComments( g, method.Comments.Leading, isDeprecatedMethod(method), ) g.P(clientSignature(g, method, false /* named */)) } g.P("}") g.P() } func generateClientImplementation(g *protogen.GeneratedFile, service *protogen.Service, names names) { clientOption := connectPackage.Ident("ClientOption") // Client constructor. wrapComments(g, names.ClientConstructor, " constructs a client for the ", service.Desc.FullName(), " service. By default, it uses the Connect protocol with the binary Protobuf Codec, ", "asks for gzipped responses, and sends uncompressed requests. ", "To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or ", "connect.WithGRPCWeb() options.") g.P("//") wrapComments(g, "The URL supplied here should be the base URL for the Connect or gRPC server ", "(for example, http://api.acme.com or https://acme.com/grpc).") if isDeprecatedService(service) { g.P("//") deprecated(g) } g.P("func ", names.ClientConstructor, " (httpClient ", connectPackage.Ident("HTTPClient"), ", baseURL string, opts ...", clientOption, ") ", names.Client, " {") if len(service.Methods) > 0 { g.P("baseURL = ", stringsPackage.Ident("TrimRight"), `(baseURL, "/")`) } g.P("return &", names.ClientImpl, "{") for _, method := range service.Methods { g.P(unexport(method.GoName), ": ", connectPackage.Ident("NewClient"), "[", method.Input.GoIdent, ", ", method.Output.GoIdent, "]", "(", ) g.P("httpClient,") g.P(`baseURL + `, procedureConstName(method), `,`) g.P(connectPackage.Ident("WithSchema"), "(", procedureVarMethodDescriptor(method), "),") idempotency := methodIdempotency(method) switch idempotency { case connect.IdempotencyNoSideEffects: g.P(connectPackage.Ident("WithIdempotency"), "(", connectPackage.Ident("IdempotencyNoSideEffects"), "),") case connect.IdempotencyIdempotent: g.P(connectPackage.Ident("WithIdempotency"), "(", connectPackage.Ident("IdempotencyIdempotent"), "),") case connect.IdempotencyUnknown: } g.P(connectPackage.Ident("WithClientOptions"), "(opts...),") g.P("),") } g.P("}") g.P("}") g.P() // Client struct. wrapComments(g, names.ClientImpl, " implements ", names.Client, ".") g.P("type ", names.ClientImpl, " struct {") for _, method := range service.Methods { g.P(unexport(method.GoName), " *", connectPackage.Ident("Client"), "[", method.Input.GoIdent, ", ", method.Output.GoIdent, "]") } g.P("}") g.P() for _, method := range service.Methods { generateClientMethod(g, method, names) } } func generateClientMethod(g *protogen.GeneratedFile, method *protogen.Method, names names) { receiver := names.ClientImpl isStreamingClient := method.Desc.IsStreamingClient() isStreamingServer := method.Desc.IsStreamingServer() wrapComments(g, method.GoName, " calls ", method.Desc.FullName(), ".") if isDeprecatedMethod(method) { g.P("//") deprecated(g) } g.P("func (c *", receiver, ") ", clientSignature(g, method, true /* named */), " {") switch { case isStreamingClient && !isStreamingServer: g.P("return c.", unexport(method.GoName), ".CallClientStream(ctx)") case !isStreamingClient && isStreamingServer: g.P("return c.", unexport(method.GoName), ".CallServerStream(ctx, req)") case isStreamingClient && isStreamingServer: g.P("return c.", unexport(method.GoName), ".CallBidiStream(ctx)") default: g.P("return c.", unexport(method.GoName), ".CallUnary(ctx, req)") } g.P("}") g.P() } func clientSignature(g *protogen.GeneratedFile, method *protogen.Method, named bool) string { reqName := "req" ctxName := "ctx" if !named { reqName, ctxName = "", "" } if method.Desc.IsStreamingClient() && method.Desc.IsStreamingServer() { // bidi streaming return method.GoName + "(" + ctxName + " " + g.QualifiedGoIdent(contextPackage.Ident("Context")) + ") " + "*" + g.QualifiedGoIdent(connectPackage.Ident("BidiStreamForClient")) + "[" + g.QualifiedGoIdent(method.Input.GoIdent) + ", " + g.QualifiedGoIdent(method.Output.GoIdent) + "]" } if method.Desc.IsStreamingClient() { // client streaming return method.GoName + "(" + ctxName + " " + g.QualifiedGoIdent(contextPackage.Ident("Context")) + ") " + "*" + g.QualifiedGoIdent(connectPackage.Ident("ClientStreamForClient")) + "[" + g.QualifiedGoIdent(method.Input.GoIdent) + ", " + g.QualifiedGoIdent(method.Output.GoIdent) + "]" } if method.Desc.IsStreamingServer() { return method.GoName + "(" + ctxName + " " + g.QualifiedGoIdent(contextPackage.Ident("Context")) + ", " + reqName + " *" + g.QualifiedGoIdent(connectPackage.Ident("Request")) + "[" + g.QualifiedGoIdent(method.Input.GoIdent) + "]) " + "(*" + g.QualifiedGoIdent(connectPackage.Ident("ServerStreamForClient")) + "[" + g.QualifiedGoIdent(method.Output.GoIdent) + "]" + ", error)" } // unary; symmetric so we can re-use server templating return method.GoName + serverSignatureParams(g, method, named) } func generateServerInterface(g *protogen.GeneratedFile, service *protogen.Service, names names) { wrapComments(g, names.Server, " is an implementation of the ", service.Desc.FullName(), " service.") if isDeprecatedService(service) { g.P("//") deprecated(g) } g.AnnotateSymbol(names.Server, protogen.Annotation{Location: service.Location}) g.P("type ", names.Server, " interface {") for _, method := range service.Methods { leadingComments( g, method.Comments.Leading, isDeprecatedMethod(method), ) g.AnnotateSymbol(names.Server+"."+method.GoName, protogen.Annotation{Location: method.Location}) g.P(serverSignature(g, method)) } g.P("}") g.P() } func generateServerConstructor(g *protogen.GeneratedFile, service *protogen.Service, names names) { wrapComments(g, names.ServerConstructor, " builds an HTTP handler from the service implementation.", " It returns the path on which to mount the handler and the handler itself.") g.P("//") wrapComments(g, "By default, handlers support the Connect, gRPC, and gRPC-Web protocols with ", "the binary Protobuf and JSON codecs. They also support gzip compression.") if isDeprecatedService(service) { g.P("//") deprecated(g) } handlerOption := connectPackage.Ident("HandlerOption") g.P("func ", names.ServerConstructor, "(svc ", names.Server, ", opts ...", handlerOption, ") (string, ", httpPackage.Ident("Handler"), ") {") for _, method := range service.Methods { isStreamingServer := method.Desc.IsStreamingServer() isStreamingClient := method.Desc.IsStreamingClient() idempotency := methodIdempotency(method) switch { case isStreamingClient && !isStreamingServer: g.P(procedureHandlerName(method), ` := `, connectPackage.Ident("NewClientStreamHandler"), "(") case !isStreamingClient && isStreamingServer: g.P(procedureHandlerName(method), ` := `, connectPackage.Ident("NewServerStreamHandler"), "(") case isStreamingClient && isStreamingServer: g.P(procedureHandlerName(method), ` := `, connectPackage.Ident("NewBidiStreamHandler"), "(") default: g.P(procedureHandlerName(method), ` := `, connectPackage.Ident("NewUnaryHandler"), "(") } g.P(procedureConstName(method), `,`) g.P("svc.", method.GoName, ",") g.P(connectPackage.Ident("WithSchema"), "(", procedureVarMethodDescriptor(method), "),") switch idempotency { case connect.IdempotencyNoSideEffects: g.P(connectPackage.Ident("WithIdempotency"), "(", connectPackage.Ident("IdempotencyNoSideEffects"), "),") case connect.IdempotencyIdempotent: g.P(connectPackage.Ident("WithIdempotency"), "(", connectPackage.Ident("IdempotencyIdempotent"), "),") case connect.IdempotencyUnknown: } g.P(connectPackage.Ident("WithHandlerOptions"), "(opts...),") g.P(")") } g.P(`return "/`, service.Desc.FullName(), `/", `, httpPackage.Ident("HandlerFunc"), `(func(w `, httpPackage.Ident("ResponseWriter"), `, r *`, httpPackage.Ident("Request"), `){`) g.P("switch r.URL.Path {") for _, method := range service.Methods { g.P("case ", procedureConstName(method), ":") g.P(procedureHandlerName(method), ".ServeHTTP(w, r)") } g.P("default:") g.P(httpPackage.Ident("NotFound"), "(w, r)") g.P("}") g.P("})") g.P("}") g.P() } func generateUnimplementedServerImplementation(g *protogen.GeneratedFile, service *protogen.Service, names names) { wrapComments(g, names.UnimplementedServer, " returns CodeUnimplemented from all methods.") g.P("type ", names.UnimplementedServer, " struct {}") g.P() for _, method := range service.Methods { g.P("func (", names.UnimplementedServer, ") ", serverSignature(g, method), "{") if method.Desc.IsStreamingServer() { g.P("return ", connectPackage.Ident("NewError"), "(", connectPackage.Ident("CodeUnimplemented"), ", ", errorsPackage.Ident("New"), `("`, method.Desc.FullName(), ` is not implemented"))`) } else { g.P("return nil, ", connectPackage.Ident("NewError"), "(", connectPackage.Ident("CodeUnimplemented"), ", ", errorsPackage.Ident("New"), `("`, method.Desc.FullName(), ` is not implemented"))`) } g.P("}") g.P() } g.P() } func serverSignature(g *protogen.GeneratedFile, method *protogen.Method) string { return method.GoName + serverSignatureParams(g, method, false /* named */) } func serverSignatureParams(g *protogen.GeneratedFile, method *protogen.Method, named bool) string { ctxName := "ctx " reqName := "req " streamName := "stream " if !named { ctxName, reqName, streamName = "", "", "" } if method.Desc.IsStreamingClient() && method.Desc.IsStreamingServer() { // bidi streaming return "(" + ctxName + g.QualifiedGoIdent(contextPackage.Ident("Context")) + ", " + streamName + "*" + g.QualifiedGoIdent(connectPackage.Ident("BidiStream")) + "[" + g.QualifiedGoIdent(method.Input.GoIdent) + ", " + g.QualifiedGoIdent(method.Output.GoIdent) + "]" + ") error" } if method.Desc.IsStreamingClient() { // client streaming return "(" + ctxName + g.QualifiedGoIdent(contextPackage.Ident("Context")) + ", " + streamName + "*" + g.QualifiedGoIdent(connectPackage.Ident("ClientStream")) + "[" + g.QualifiedGoIdent(method.Input.GoIdent) + "]" + ") (*" + g.QualifiedGoIdent(connectPackage.Ident("Response")) + "[" + g.QualifiedGoIdent(method.Output.GoIdent) + "] ,error)" } if method.Desc.IsStreamingServer() { // server streaming return "(" + ctxName + g.QualifiedGoIdent(contextPackage.Ident("Context")) + ", " + reqName + "*" + g.QualifiedGoIdent(connectPackage.Ident("Request")) + "[" + g.QualifiedGoIdent(method.Input.GoIdent) + "], " + streamName + "*" + g.QualifiedGoIdent(connectPackage.Ident("ServerStream")) + "[" + g.QualifiedGoIdent(method.Output.GoIdent) + "]" + ") error" } // unary return "(" + ctxName + g.QualifiedGoIdent(contextPackage.Ident("Context")) + ", " + reqName + "*" + g.QualifiedGoIdent(connectPackage.Ident("Request")) + "[" + g.QualifiedGoIdent(method.Input.GoIdent) + "]) " + "(*" + g.QualifiedGoIdent(connectPackage.Ident("Response")) + "[" + g.QualifiedGoIdent(method.Output.GoIdent) + "], error)" } func procedureConstName(m *protogen.Method) string { return fmt.Sprintf("%s%sProcedure", m.Parent.GoName, m.GoName) } func procedureHandlerName(m *protogen.Method) string { return fmt.Sprintf("%s%sHandler", unexport(m.Parent.GoName), m.GoName) } func procedureVarMethodDescriptor(m *protogen.Method) string { return unexport(fmt.Sprintf("%s%sMethodDescriptor", m.Parent.GoName, m.GoName)) } func isDeprecatedService(service *protogen.Service) bool { serviceOptions, ok := service.Desc.Options().(*descriptorpb.ServiceOptions) return ok && serviceOptions.GetDeprecated() } func isDeprecatedMethod(method *protogen.Method) bool { methodOptions, ok := method.Desc.Options().(*descriptorpb.MethodOptions) return ok && methodOptions.GetDeprecated() } func methodIdempotency(method *protogen.Method) connect.IdempotencyLevel { methodOptions, ok := method.Desc.Options().(*descriptorpb.MethodOptions) if !ok { return connect.IdempotencyUnknown } switch methodOptions.GetIdempotencyLevel() { case descriptorpb.MethodOptions_NO_SIDE_EFFECTS: return connect.IdempotencyNoSideEffects case descriptorpb.MethodOptions_IDEMPOTENT: return connect.IdempotencyIdempotent case descriptorpb.MethodOptions_IDEMPOTENCY_UNKNOWN: return connect.IdempotencyUnknown } return connect.IdempotencyUnknown } // Raggedy comments in the generated code are driving me insane. This // word-wrapping function is ruinously inefficient, but it gets the job done. func wrapComments(g *protogen.GeneratedFile, elems ...any) { text := &bytes.Buffer{} for _, el := range elems { switch el := el.(type) { case protogen.GoIdent: fmt.Fprint(text, g.QualifiedGoIdent(el)) default: fmt.Fprint(text, el) } } words := strings.Fields(text.String()) text.Reset() var pos int for _, word := range words { numRunes := utf8.RuneCountInString(word) if pos > 0 && pos+numRunes+1 > commentWidth { g.P("// ", text.String()) text.Reset() pos = 0 } if pos > 0 { text.WriteRune(' ') pos++ } text.WriteString(word) pos += numRunes } if text.Len() > 0 { g.P("// ", text.String()) } } func leadingComments(g *protogen.GeneratedFile, comments protogen.Comments, isDeprecated bool) { if comments.String() != "" { g.P(strings.TrimSpace(comments.String())) } if isDeprecated { if comments.String() != "" { g.P("//") } deprecated(g) } } func deprecated(g *protogen.GeneratedFile) { g.P("// Deprecated: do not use.") } func unexport(s string) string { lowercased := strings.ToLower(s[:1]) + s[1:] switch lowercased { // https://go.dev/ref/spec#Keywords case "break", "default", "func", "interface", "select", "case", "defer", "go", "map", "struct", "chan", "else", "goto", "package", "switch", "const", "fallthrough", "if", "range", "type", "continue", "for", "import", "return", "var": return "_" + lowercased default: return lowercased } } type names struct { Base string Client string ClientConstructor string ClientImpl string ClientExposeMethod string Server string ServerConstructor string UnimplementedServer string } func newNames(service *protogen.Service) names { base := service.GoName return names{ Base: base, Client: fmt.Sprintf("%sClient", base), ClientConstructor: fmt.Sprintf("New%sClient", base), ClientImpl: fmt.Sprintf("%sClient", unexport(base)), Server: fmt.Sprintf("%sHandler", base), ServerConstructor: fmt.Sprintf("New%sHandler", base), UnimplementedServer: fmt.Sprintf("Unimplemented%sHandler", base), } } connect-go-1.13.0/code.go000066400000000000000000000147711453471351600151140ustar00rootroot00000000000000// Copyright 2021-2023 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 strings.HasPrefix(dataStr, "code_") { dataStr = strings.TrimPrefix(dataStr, "code_") code, err := strconv.ParseInt(dataStr, 10 /* base */, 64 /* bitsize */) if err == nil && (code < int64(minCode) || code > int64(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 } connect-go-1.13.0/code_test.go000066400000000000000000000032021453471351600161360ustar00rootroot00000000000000// Copyright 2021-2023 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 ( "strconv" "strings" "testing" "connectrpc.com/connect/internal/assert" ) func TestCode(t *testing.T) { t.Parallel() var valid []Code for code := minCode; code <= maxCode; code++ { valid = append(valid, code) } // Ensures that we don't forget to update the mapping in the Stringer // implementation. for _, code := range valid { assert.False( t, strings.HasPrefix(code.String(), "code_"), assert.Sprintf("update Code.String() method for new code %v", code), ) assertCodeRoundTrips(t, code) } assertCodeRoundTrips(t, Code(999)) } func assertCodeRoundTrips(tb testing.TB, code Code) { tb.Helper() encoded, err := code.MarshalText() assert.Nil(tb, err) var decoded Code assert.Nil(tb, decoded.UnmarshalText(encoded)) assert.Equal(tb, decoded, code) if code >= minCode && code <= maxCode { var invalid Code // For the known codes, we only accept the canonical string representation: "canceled", not "code_1". assert.NotNil(tb, invalid.UnmarshalText([]byte("code_"+strconv.Itoa(int(code))))) } } connect-go-1.13.0/codec.go000066400000000000000000000175131453471351600152540ustar00rootroot00000000000000// Copyright 2021-2023 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) } connect-go-1.13.0/codec_test.go000066400000000000000000000101211453471351600162770ustar00rootroot00000000000000// Copyright 2021-2023 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" "strings" "testing" "testing/quick" "connectrpc.com/connect/internal/assert" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/structpb" ) func convertMapToInterface(stringMap map[string]string) map[string]interface{} { interfaceMap := make(map[string]interface{}) for key, value := range stringMap { interfaceMap[key] = value } return interfaceMap } func TestCodecRoundTrips(t *testing.T) { t.Parallel() makeRoundtrip := func(codec Codec) func(string, int64) bool { return func(text string, number int64) bool { got := pingv1.PingRequest{} want := pingv1.PingRequest{Text: text, Number: number} data, err := codec.Marshal(&want) if err != nil { t.Fatal(err) } err = codec.Unmarshal(data, &got) if err != nil { t.Fatal(err) } return proto.Equal(&got, &want) } } if err := quick.Check(makeRoundtrip(&protoBinaryCodec{}), nil /* config */); err != nil { t.Error(err) } if err := quick.Check(makeRoundtrip(&protoJSONCodec{}), nil /* config */); err != nil { t.Error(err) } } func TestAppendCodec(t *testing.T) { t.Parallel() makeRoundtrip := func(codec marshalAppender) func(string, int64) bool { var data []byte return func(text string, number int64) bool { got := pingv1.PingRequest{} want := pingv1.PingRequest{Text: text, Number: number} data = data[:0] var err error data, err = codec.MarshalAppend(data, &want) if err != nil { t.Fatal(err) } err = codec.Unmarshal(data, &got) if err != nil { t.Fatal(err) } return proto.Equal(&got, &want) } } if err := quick.Check(makeRoundtrip(&protoBinaryCodec{}), nil /* config */); err != nil { t.Error(err) } if err := quick.Check(makeRoundtrip(&protoJSONCodec{}), nil /* config */); err != nil { t.Error(err) } } func TestStableCodec(t *testing.T) { t.Parallel() makeRoundtrip := func(codec stableCodec) func(map[string]string) bool { return func(input map[string]string) bool { initialProto, err := structpb.NewStruct(convertMapToInterface(input)) if err != nil { t.Fatal(err) } want, err := codec.MarshalStable(initialProto) if err != nil { t.Fatal(err) } for i := 0; i < 10; i++ { roundtripProto := &structpb.Struct{} err = codec.Unmarshal(want, roundtripProto) if err != nil { t.Fatal(err) } got, err := codec.MarshalStable(roundtripProto) if err != nil { t.Fatal(err) } if !bytes.Equal(got, want) { return false } } return true } } if err := quick.Check(makeRoundtrip(&protoBinaryCodec{}), nil /* config */); err != nil { t.Error(err) } if err := quick.Check(makeRoundtrip(&protoJSONCodec{}), nil /* config */); err != nil { t.Error(err) } } func TestJSONCodec(t *testing.T) { t.Parallel() codec := &protoJSONCodec{name: "json"} t.Run("success", func(t *testing.T) { t.Parallel() err := codec.Unmarshal([]byte("{}"), &emptypb.Empty{}) assert.Nil(t, err) }) t.Run("unknown fields", func(t *testing.T) { t.Parallel() err := codec.Unmarshal([]byte(`{"foo": "bar"}`), &emptypb.Empty{}) assert.Nil(t, err) }) t.Run("empty string", func(t *testing.T) { t.Parallel() err := codec.Unmarshal([]byte{}, &emptypb.Empty{}) assert.NotNil(t, err) assert.True( t, strings.Contains(err.Error(), "valid JSON"), assert.Sprintf(`error message should explain that "" is not a valid JSON object`), ) }) } connect-go-1.13.0/compression.go000066400000000000000000000153351453471351600165400ustar00rootroot00000000000000// Copyright 2021-2023 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" "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(strings.NewReader("")) 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 } connect-go-1.13.0/compression_test.go000066400000000000000000000126711453471351600175770ustar00rootroot00000000000000// Copyright 2021-2023 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" "testing" "connectrpc.com/connect/internal/assert" "connectrpc.com/connect/internal/memhttp/memhttptest" "google.golang.org/protobuf/types/known/emptypb" ) func TestAcceptEncodingOrdering(t *testing.T) { t.Parallel() const ( compressionBrotli = "br" expect = compressionGzip + "," + compressionBrotli ) withFakeBrotli, ok := withGzip().(*compressionOption) assert.True(t, ok) withFakeBrotli.Name = compressionBrotli var called bool verify := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { got := r.Header.Get(connectUnaryHeaderAcceptCompression) assert.Equal(t, got, expect) w.WriteHeader(http.StatusOK) called = true }) server := memhttptest.NewServer(t, verify) client := NewClient[emptypb.Empty, emptypb.Empty]( server.Client(), server.URL(), withFakeBrotli, withGzip(), ) _, _ = client.CallUnary(context.Background(), NewRequest(&emptypb.Empty{})) assert.True(t, called) } func TestClientCompressionOptionTest(t *testing.T) { t.Parallel() const testURL = "http://foo.bar.com/service/method" checkPools := func(t *testing.T, config *clientConfig) { t.Helper() assert.Equal(t, len(config.CompressionNames), len(config.CompressionPools)) for _, name := range config.CompressionNames { pool := config.CompressionPools[name] assert.NotNil(t, pool) } } dummyDecompressCtor := func() Decompressor { return nil } dummyCompressCtor := func() Compressor { return nil } t.Run("defaults", func(t *testing.T) { t.Parallel() config, err := newClientConfig(testURL, nil) assert.Nil(t, err) assert.Equal(t, config.CompressionNames, []string{compressionGzip}) checkPools(t, config) }) t.Run("WithAcceptCompression", func(t *testing.T) { t.Parallel() opts := []ClientOption{WithAcceptCompression("foo", dummyDecompressCtor, dummyCompressCtor)} config, err := newClientConfig(testURL, opts) assert.Nil(t, err) assert.Equal(t, config.CompressionNames, []string{compressionGzip, "foo"}) checkPools(t, config) }) t.Run("WithAcceptCompression-empty-name-noop", func(t *testing.T) { t.Parallel() opts := []ClientOption{WithAcceptCompression("", dummyDecompressCtor, dummyCompressCtor)} config, err := newClientConfig(testURL, opts) assert.Nil(t, err) assert.Equal(t, config.CompressionNames, []string{compressionGzip}) checkPools(t, config) }) t.Run("WithAcceptCompression-nil-ctors-noop", func(t *testing.T) { t.Parallel() opts := []ClientOption{WithAcceptCompression("foo", nil, nil)} config, err := newClientConfig(testURL, opts) assert.Nil(t, err) assert.Equal(t, config.CompressionNames, []string{compressionGzip}) checkPools(t, config) }) t.Run("WithAcceptCompression-nil-ctors-unregisters", func(t *testing.T) { t.Parallel() opts := []ClientOption{WithAcceptCompression("gzip", nil, nil)} config, err := newClientConfig(testURL, opts) assert.Nil(t, err) assert.Equal(t, config.CompressionNames, nil) checkPools(t, config) }) } func TestHandlerCompressionOptionTest(t *testing.T) { t.Parallel() const testProc = "/service/method" checkPools := func(t *testing.T, config *handlerConfig) { t.Helper() assert.Equal(t, len(config.CompressionNames), len(config.CompressionPools)) for _, name := range config.CompressionNames { pool := config.CompressionPools[name] assert.NotNil(t, pool) } } dummyDecompressCtor := func() Decompressor { return nil } dummyCompressCtor := func() Compressor { return nil } t.Run("defaults", func(t *testing.T) { t.Parallel() config := newHandlerConfig(testProc, StreamTypeUnary, nil) assert.Equal(t, config.CompressionNames, []string{compressionGzip}) checkPools(t, config) }) t.Run("WithCompression", func(t *testing.T) { t.Parallel() opts := []HandlerOption{WithCompression("foo", dummyDecompressCtor, dummyCompressCtor)} config := newHandlerConfig(testProc, StreamTypeUnary, opts) assert.Equal(t, config.CompressionNames, []string{compressionGzip, "foo"}) checkPools(t, config) }) t.Run("WithCompression-empty-name-noop", func(t *testing.T) { t.Parallel() opts := []HandlerOption{WithCompression("", dummyDecompressCtor, dummyCompressCtor)} config := newHandlerConfig(testProc, StreamTypeUnary, opts) assert.Equal(t, config.CompressionNames, []string{compressionGzip}) checkPools(t, config) }) t.Run("WithCompression-nil-ctors-noop", func(t *testing.T) { t.Parallel() opts := []HandlerOption{WithCompression("foo", nil, nil)} config := newHandlerConfig(testProc, StreamTypeUnary, opts) assert.Equal(t, config.CompressionNames, []string{compressionGzip}) checkPools(t, config) }) t.Run("WithCompression-nil-ctors-unregisters", func(t *testing.T) { t.Parallel() opts := []HandlerOption{WithCompression("gzip", nil, nil)} config := newHandlerConfig(testProc, StreamTypeUnary, opts) assert.Equal(t, config.CompressionNames, nil) checkPools(t, config) }) } connect-go-1.13.0/connect.go000066400000000000000000000314671453471351600156340ustar00rootroot00000000000000// Copyright 2021-2023 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.13.0" // 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 newPeerFromURL(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 } // 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) { var msg T if err := initializer.maybe(conn.Spec(), &msg); err != nil { return nil, err } if err := conn.Receive(&msg); err != nil { return nil, err } // In a well-formed stream, the response message may be followed by a block // of in-stream trailers or HTTP trailers. To ensure that we receive the // trailers, try to read another message from the stream. // TODO: optimise unary calls to avoid this extra receive. var msg2 T if err := initializer.maybe(conn.Spec(), &msg2); err != nil { return nil, err } if err := conn.Receive(&msg2); err == nil { return nil, NewError(CodeUnknown, errors.New("unary stream has multiple messages")) } else if err != nil && !errors.Is(err, io.EOF) { return nil, err } return &Response[T]{ Msg: &msg, header: conn.ResponseHeader(), trailer: conn.ResponseTrailer(), }, nil } connect-go-1.13.0/connect_ext_test.go000066400000000000000000002672521453471351600175560ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "bytes" "compress/flate" "compress/gzip" "context" "encoding/binary" "errors" "fmt" "io" "math" "math/rand" "net/http" "runtime" "strings" "sync" "testing" "time" connect "connectrpc.com/connect" "connectrpc.com/connect/internal/assert" "connectrpc.com/connect/internal/gen/connect/import/v1/importv1connect" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" "connectrpc.com/connect/internal/memhttp" "connectrpc.com/connect/internal/memhttp/memhttptest" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoregistry" ) const errorMessage = "oh no" // The ping server implementation used in the tests returns errors if the // client doesn't set a header, and the server sets headers and trailers on the // response. const ( headerValue = "some header value" trailerValue = "some trailer value" clientHeader = "Connect-Client-Header" handlerHeader = "Connect-Handler-Header" handlerTrailer = "Connect-Handler-Trailer" clientMiddlewareErrorHeader = "Connect-Trigger-HTTP-Error" ) func TestServer(t *testing.T) { t.Parallel() testPing := func(t *testing.T, client pingv1connect.PingServiceClient) { //nolint:thelper t.Run("ping", func(t *testing.T) { num := int64(42) request := connect.NewRequest(&pingv1.PingRequest{Number: num}) request.Header().Set(clientHeader, headerValue) expect := &pingv1.PingResponse{Number: num} response, err := client.Ping(context.Background(), request) assert.Nil(t, err) assert.Equal(t, response.Msg, expect) assert.Equal(t, response.Header().Values(handlerHeader), []string{headerValue}) assert.Equal(t, response.Trailer().Values(handlerTrailer), []string{trailerValue}) }) t.Run("zero_ping", func(t *testing.T) { request := connect.NewRequest(&pingv1.PingRequest{}) request.Header().Set(clientHeader, headerValue) response, err := client.Ping(context.Background(), request) assert.Nil(t, err) var expect pingv1.PingResponse assert.Equal(t, response.Msg, &expect) assert.Equal(t, response.Header().Values(handlerHeader), []string{headerValue}) assert.Equal(t, response.Trailer().Values(handlerTrailer), []string{trailerValue}) }) t.Run("large_ping", func(t *testing.T) { // Using a large payload splits the request and response over multiple // packets, ensuring that we're managing HTTP readers and writers // correctly. if testing.Short() { t.Skipf("skipping %s test in short mode", t.Name()) } hellos := strings.Repeat("hello", 1024*1024) // ~5mb request := connect.NewRequest(&pingv1.PingRequest{Text: hellos}) request.Header().Set(clientHeader, headerValue) response, err := client.Ping(context.Background(), request) assert.Nil(t, err) assert.Equal(t, response.Msg.GetText(), hellos) assert.Equal(t, response.Header().Values(handlerHeader), []string{headerValue}) assert.Equal(t, response.Trailer().Values(handlerTrailer), []string{trailerValue}) }) t.Run("ping_error", func(t *testing.T) { _, err := client.Ping( context.Background(), connect.NewRequest(&pingv1.PingRequest{}), ) assert.Equal(t, connect.CodeOf(err), connect.CodeInvalidArgument) }) t.Run("ping_timeout", func(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second)) defer cancel() request := connect.NewRequest(&pingv1.PingRequest{}) request.Header().Set(clientHeader, headerValue) _, err := client.Ping(ctx, request) assert.Equal(t, connect.CodeOf(err), connect.CodeDeadlineExceeded) }) } testSum := func(t *testing.T, client pingv1connect.PingServiceClient) { //nolint:thelper t.Run("sum", func(t *testing.T) { const ( upTo = 10 expect = 55 // 1+10 + 2+9 + ... + 5+6 = 55 ) stream := client.Sum(context.Background()) stream.RequestHeader().Set(clientHeader, headerValue) for i := int64(1); i <= upTo; i++ { err := stream.Send(&pingv1.SumRequest{Number: i}) assert.Nil(t, err, assert.Sprintf("send %d", i)) } response, err := stream.CloseAndReceive() assert.Nil(t, err) assert.Equal(t, response.Msg.GetSum(), expect) assert.Equal(t, response.Header().Values(handlerHeader), []string{headerValue}) assert.Equal(t, response.Trailer().Values(handlerTrailer), []string{trailerValue}) }) t.Run("sum_error", func(t *testing.T) { stream := client.Sum(context.Background()) if err := stream.Send(&pingv1.SumRequest{Number: 1}); err != nil { assert.ErrorIs(t, err, io.EOF) assert.Equal(t, connect.CodeOf(err), connect.CodeUnknown) } _, err := stream.CloseAndReceive() assert.Equal(t, connect.CodeOf(err), connect.CodeInvalidArgument) }) t.Run("sum_close_and_receive_without_send", func(t *testing.T) { stream := client.Sum(context.Background()) stream.RequestHeader().Set(clientHeader, headerValue) got, err := stream.CloseAndReceive() assert.Nil(t, err) assert.Equal(t, got.Msg, &pingv1.SumResponse{}) // receive header only stream assert.Equal(t, got.Header().Values(handlerHeader), []string{headerValue}) }) } testCountUp := func(t *testing.T, client pingv1connect.PingServiceClient) { //nolint:thelper t.Run("count_up", func(t *testing.T) { const upTo = 5 got := make([]int64, 0, upTo) expect := make([]int64, 0, upTo) for i := 1; i <= upTo; i++ { expect = append(expect, int64(i)) } request := connect.NewRequest(&pingv1.CountUpRequest{Number: upTo}) request.Header().Set(clientHeader, headerValue) stream, err := client.CountUp(context.Background(), request) assert.Nil(t, err) for stream.Receive() { got = append(got, stream.Msg().GetNumber()) } assert.Nil(t, stream.Err()) assert.Nil(t, stream.Close()) assert.Equal(t, got, expect) }) t.Run("count_up_error", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) stream, err := client.CountUp( ctx, connect.NewRequest(&pingv1.CountUpRequest{Number: 1}), ) assert.Nil(t, err) for stream.Receive() { t.Fatalf("expected error, shouldn't receive any messages") } assert.Equal( t, connect.CodeOf(stream.Err()), connect.CodeInvalidArgument, ) assert.Nil(t, stream.Close()) }) t.Run("count_up_timeout", func(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second)) t.Cleanup(cancel) _, err := client.CountUp(ctx, connect.NewRequest(&pingv1.CountUpRequest{Number: 1})) assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeDeadlineExceeded) }) t.Run("count_up_cancel_after_first_response", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) request := connect.NewRequest(&pingv1.CountUpRequest{Number: 5}) request.Header().Set(clientHeader, headerValue) stream, err := client.CountUp(ctx, request) assert.Nil(t, err) assert.True(t, stream.Receive()) cancel() assert.False(t, stream.Receive()) assert.NotNil(t, stream.Err()) assert.Equal(t, connect.CodeOf(stream.Err()), connect.CodeCanceled) assert.Nil(t, stream.Close()) }) } testCumSum := func(t *testing.T, client pingv1connect.PingServiceClient, expectSuccess bool) { //nolint:thelper t.Run("cumsum", func(t *testing.T) { send := []int64{3, 5, 1} expect := []int64{3, 8, 9} var got []int64 stream := client.CumSum(context.Background()) stream.RequestHeader().Set(clientHeader, headerValue) if !expectSuccess { // server doesn't support HTTP/2 failNoHTTP2(t, stream) return } var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() for i, n := range send { err := stream.Send(&pingv1.CumSumRequest{Number: n}) assert.Nil(t, err, assert.Sprintf("send error #%d", i)) } assert.Nil(t, stream.CloseRequest()) }() go func() { defer wg.Done() for { msg, err := stream.Receive() if errors.Is(err, io.EOF) { break } assert.Nil(t, err) got = append(got, msg.GetSum()) } assert.Nil(t, stream.CloseResponse()) }() wg.Wait() assert.Equal(t, got, expect) assert.Equal(t, stream.ResponseHeader().Values(handlerHeader), []string{headerValue}) assert.Equal(t, stream.ResponseTrailer().Values(handlerTrailer), []string{trailerValue}) }) t.Run("cumsum_error", func(t *testing.T) { stream := client.CumSum(context.Background()) if !expectSuccess { // server doesn't support HTTP/2 failNoHTTP2(t, stream) return } if err := stream.Send(&pingv1.CumSumRequest{Number: 42}); err != nil { assert.ErrorIs(t, err, io.EOF) assert.Equal(t, connect.CodeOf(err), connect.CodeUnknown) } // We didn't send the headers the server expects, so we should now get an // error. _, err := stream.Receive() assert.Equal(t, connect.CodeOf(err), connect.CodeInvalidArgument) assert.True(t, connect.IsWireError(err)) }) t.Run("cumsum_empty_stream", func(t *testing.T) { stream := client.CumSum(context.Background()) stream.RequestHeader().Set(clientHeader, headerValue) if !expectSuccess { // server doesn't support HTTP/2 failNoHTTP2(t, stream) return } // Deliberately closing with calling Send to test the behavior of Receive. // This test case is based on the grpc interop tests. assert.Nil(t, stream.CloseRequest()) response, err := stream.Receive() assert.Nil(t, response) assert.True(t, errors.Is(err, io.EOF)) assert.False(t, connect.IsWireError(err)) assert.Nil(t, stream.CloseResponse()) // clean-up the stream }) t.Run("cumsum_cancel_after_first_response", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) stream := client.CumSum(ctx) stream.RequestHeader().Set(clientHeader, headerValue) if !expectSuccess { // server doesn't support HTTP/2 failNoHTTP2(t, stream) cancel() return } var got []int64 expect := []int64{42} if err := stream.Send(&pingv1.CumSumRequest{Number: 42}); err != nil { assert.ErrorIs(t, err, io.EOF) assert.Equal(t, connect.CodeOf(err), connect.CodeUnknown) } msg, err := stream.Receive() assert.Nil(t, err) got = append(got, msg.GetSum()) cancel() _, err = stream.Receive() assert.Equal(t, connect.CodeOf(err), connect.CodeCanceled) assert.Equal(t, got, expect) assert.False(t, connect.IsWireError(err)) assert.Nil(t, stream.CloseResponse()) }) t.Run("cumsum_cancel_before_send", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) stream := client.CumSum(ctx) if !expectSuccess { // server doesn't support HTTP/2 failNoHTTP2(t, stream) cancel() return } stream.RequestHeader().Set(clientHeader, headerValue) assert.Nil(t, stream.Send(&pingv1.CumSumRequest{Number: 8})) cancel() // On a subsequent send, ensure that we are still catching context // cancellations. err := stream.Send(&pingv1.CumSumRequest{Number: 19}) assert.Equal(t, connect.CodeOf(err), connect.CodeCanceled, assert.Sprintf("%v", err)) assert.False(t, connect.IsWireError(err)) assert.Nil(t, stream.CloseRequest()) assert.Nil(t, stream.CloseResponse()) }) } testErrors := func(t *testing.T, client pingv1connect.PingServiceClient) { //nolint:thelper assertIsHTTPMiddlewareError := func(tb testing.TB, err error) { tb.Helper() assert.NotNil(tb, err) var connectErr *connect.Error assert.True(tb, errors.As(err, &connectErr)) expect := newHTTPMiddlewareError() assert.Equal(tb, connectErr.Code(), expect.Code()) assert.Equal(tb, connectErr.Message(), expect.Message()) for k, v := range expect.Meta() { assert.Equal(tb, connectErr.Meta().Values(k), v) } assert.Equal(tb, len(connectErr.Details()), len(expect.Details())) } t.Run("errors", func(t *testing.T) { request := connect.NewRequest(&pingv1.FailRequest{ Code: int32(connect.CodeResourceExhausted), }) request.Header().Set(clientHeader, headerValue) response, err := client.Fail(context.Background(), request) assert.Nil(t, response) assert.NotNil(t, err) var connectErr *connect.Error ok := errors.As(err, &connectErr) assert.True(t, ok, assert.Sprintf("conversion to *connect.Error")) assert.True(t, connect.IsWireError(err)) assert.Equal(t, connectErr.Code(), connect.CodeResourceExhausted) assert.Equal(t, connectErr.Error(), "resource_exhausted: "+errorMessage) assert.Zero(t, connectErr.Details()) assert.Equal(t, connectErr.Meta().Values(handlerHeader), []string{headerValue}) assert.Equal(t, connectErr.Meta().Values(handlerTrailer), []string{trailerValue}) }) t.Run("middleware_errors_unary", func(t *testing.T) { request := connect.NewRequest(&pingv1.PingRequest{}) request.Header().Set(clientMiddlewareErrorHeader, headerValue) _, err := client.Ping(context.Background(), request) assertIsHTTPMiddlewareError(t, err) }) t.Run("middleware_errors_streaming", func(t *testing.T) { request := connect.NewRequest(&pingv1.CountUpRequest{Number: 10}) request.Header().Set(clientMiddlewareErrorHeader, headerValue) stream, err := client.CountUp(context.Background(), request) assert.Nil(t, err) assert.False(t, stream.Receive()) assertIsHTTPMiddlewareError(t, stream.Err()) }) } testMatrix := func(t *testing.T, client *http.Client, url string, bidi bool) { //nolint:thelper run := func(t *testing.T, opts ...connect.ClientOption) { t.Helper() client := pingv1connect.NewPingServiceClient(client, url, opts...) testPing(t, client) testSum(t, client) testCountUp(t, client) testCumSum(t, client, bidi) testErrors(t, client) } t.Run("connect", func(t *testing.T) { t.Run("proto", func(t *testing.T) { run(t) }) t.Run("proto_gzip", func(t *testing.T) { run(t, connect.WithSendGzip()) }) t.Run("json_gzip", func(t *testing.T) { run( t, connect.WithProtoJSON(), connect.WithSendGzip(), ) }) }) t.Run("grpc", func(t *testing.T) { t.Run("proto", func(t *testing.T) { run(t, connect.WithGRPC()) }) t.Run("proto_gzip", func(t *testing.T) { run(t, connect.WithGRPC(), connect.WithSendGzip()) }) t.Run("json_gzip", func(t *testing.T) { run( t, connect.WithGRPC(), connect.WithProtoJSON(), connect.WithSendGzip(), ) }) }) t.Run("grpcweb", func(t *testing.T) { t.Run("proto", func(t *testing.T) { run(t, connect.WithGRPCWeb()) }) t.Run("proto_gzip", func(t *testing.T) { run(t, connect.WithGRPCWeb(), connect.WithSendGzip()) }) t.Run("json_gzip", func(t *testing.T) { run( t, connect.WithGRPCWeb(), connect.WithProtoJSON(), connect.WithSendGzip(), ) }) }) } mux := http.NewServeMux() pingRoute, pingHandler := pingv1connect.NewPingServiceHandler( pingServer{checkMetadata: true}, ) errorWriter := connect.NewErrorWriter() // Add some net/http middleware to the ping service so we can also exercise ErrorWriter. mux.Handle(pingRoute, http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { if request.Header.Get(clientMiddlewareErrorHeader) != "" { defer request.Body.Close() if _, err := io.Copy(io.Discard, request.Body); err != nil { t.Errorf("drain request body: %v", err) } if !errorWriter.IsSupported(request) { t.Errorf("ErrorWriter doesn't support Content-Type %q", request.Header.Get("Content-Type")) } if err := errorWriter.Write(response, request, newHTTPMiddlewareError()); err != nil { t.Errorf("send RPC error from HTTP middleware: %v", err) } return } pingHandler.ServeHTTP(response, request) })) t.Run("http1", func(t *testing.T) { t.Parallel() server := memhttptest.NewServer(t, mux) client := &http.Client{Transport: server.TransportHTTP1()} testMatrix(t, client, server.URL(), false /* bidi */) }) t.Run("http2", func(t *testing.T) { t.Parallel() server := memhttptest.NewServer(t, mux) client := server.Client() testMatrix(t, client, server.URL(), true /* bidi */) }) } func TestConcurrentStreams(t *testing.T) { if testing.Short() { t.Skipf("skipping %s test in short mode", t.Name()) } t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) server := memhttptest.NewServer(t, mux) var done, start sync.WaitGroup start.Add(1) for i := 0; i < runtime.GOMAXPROCS(0)*8; i++ { done.Add(1) go func() { defer done.Done() client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) var total int64 sum := client.CumSum(context.Background()) start.Wait() for i := 0; i < 100; i++ { num := rand.Int63n(1000) //nolint: gosec total += num if err := sum.Send(&pingv1.CumSumRequest{Number: num}); err != nil { t.Errorf("failed to send request: %v", err) break } resp, err := sum.Receive() if err != nil { t.Errorf("failed to receive from stream: %v", err) break } if got := resp.GetSum(); total != got { t.Errorf("expected %d == %d", total, got) break } } if err := sum.CloseRequest(); err != nil { t.Errorf("failed to close request: %v", err) } if err := sum.CloseResponse(); err != nil { t.Errorf("failed to close response: %v", err) } }() } start.Done() done.Wait() } func TestHeaderBasic(t *testing.T) { t.Parallel() const ( key = "Test-Key" cval = "client value" hval = "client value" ) pingServer := &pluggablePingServer{ ping: func(ctx context.Context, request *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { assert.Equal(t, request.Header().Get(key), cval) response := connect.NewResponse(&pingv1.PingResponse{}) response.Header().Set(key, hval) return response, nil }, } mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer)) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) request := connect.NewRequest(&pingv1.PingRequest{}) request.Header().Set(key, cval) response, err := client.Ping(context.Background(), request) assert.Nil(t, err) assert.Equal(t, response.Header().Get(key), hval) } func TestHeaderHost(t *testing.T) { t.Parallel() const ( key = "Host" cval = "buf.build" ) pingServer := &pluggablePingServer{ ping: func(_ context.Context, request *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { assert.Equal(t, request.Header().Get(key), cval) response := connect.NewResponse(&pingv1.PingResponse{}) return response, nil }, } newHTTP2Server := func(t *testing.T) *memhttp.Server { t.Helper() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer)) server := memhttptest.NewServer(t, mux) return server } callWithHost := func(t *testing.T, client pingv1connect.PingServiceClient) { t.Helper() request := connect.NewRequest(&pingv1.PingRequest{}) request.Header().Set(key, cval) response, err := client.Ping(context.Background(), request) assert.Nil(t, err) assert.Equal(t, response.Header().Get(key), "") } t.Run("connect", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) callWithHost(t, client) }) t.Run("grpc", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPC()) callWithHost(t, client) }) t.Run("grpc-web", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPCWeb()) callWithHost(t, client) }) } func TestTimeoutParsing(t *testing.T) { t.Parallel() const timeout = 10 * time.Minute pingServer := &pluggablePingServer{ ping: func(ctx context.Context, request *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { deadline, ok := ctx.Deadline() assert.True(t, ok) remaining := time.Until(deadline) assert.True(t, remaining > 0) assert.True(t, remaining <= timeout) return connect.NewResponse(&pingv1.PingResponse{}), nil }, } mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer)) server := memhttptest.NewServer(t, mux) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) _, err := client.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{})) assert.Nil(t, err) } func TestFailCodec(t *testing.T) { t.Parallel() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) server := memhttptest.NewServer(t, handler) client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), connect.WithCodec(failCodec{}), ) stream := client.CumSum(context.Background()) err := stream.Send(&pingv1.CumSumRequest{}) var connectErr *connect.Error assert.NotNil(t, err) assert.True(t, errors.As(err, &connectErr)) assert.Equal(t, connectErr.Code(), connect.CodeInternal) } func TestContextError(t *testing.T) { t.Parallel() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) server := memhttptest.NewServer(t, handler) client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), ) ctx, cancel := context.WithCancel(context.Background()) cancel() stream := client.CumSum(ctx) err := stream.Send(nil) var connectErr *connect.Error assert.NotNil(t, err) assert.True(t, errors.As(err, &connectErr)) assert.Equal(t, connectErr.Code(), connect.CodeCanceled) assert.False(t, connect.IsWireError(err)) } func TestGRPCMarshalStatusError(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler( pingServer{}, connect.WithCodec(failCodec{}), )) server := memhttptest.NewServer(t, mux) assertInternalError := func(tb testing.TB, opts ...connect.ClientOption) { tb.Helper() client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), opts...) request := connect.NewRequest(&pingv1.FailRequest{Code: int32(connect.CodeResourceExhausted)}) _, err := client.Fail(context.Background(), request) tb.Log(err) assert.NotNil(t, err) var connectErr *connect.Error ok := errors.As(err, &connectErr) assert.True(t, ok) assert.Equal(t, connectErr.Code(), connect.CodeInternal) assert.True( t, strings.HasSuffix(connectErr.Message(), ": boom"), ) } // Only applies to gRPC protocols, where we're marshaling the Status protobuf // message to binary. assertInternalError(t, connect.WithGRPC()) assertInternalError(t, connect.WithGRPCWeb()) } func TestGRPCMissingTrailersError(t *testing.T) { t.Parallel() trimTrailers := func(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.Header.Del("Te") handler.ServeHTTP(&trimTrailerWriter{w: w}, r) }) } mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler( pingServer{checkMetadata: true}, )) server := memhttptest.NewServer(t, trimTrailers(mux)) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPC()) assertErrorNoTrailers := func(t *testing.T, err error) { t.Helper() assert.NotNil(t, err) var connectErr *connect.Error ok := errors.As(err, &connectErr) assert.True(t, ok) assert.Equal(t, connectErr.Code(), connect.CodeInternal) assert.True( t, strings.HasSuffix(connectErr.Message(), "protocol error: no Grpc-Status trailer: unexpected EOF"), ) } assertNilOrEOF := func(t *testing.T, err error) { t.Helper() if err != nil { assert.ErrorIs(t, err, io.EOF) } } t.Run("ping", func(t *testing.T) { t.Parallel() request := connect.NewRequest(&pingv1.PingRequest{Number: 1, Text: "foobar"}) _, err := client.Ping(context.Background(), request) assertErrorNoTrailers(t, err) }) t.Run("sum", func(t *testing.T) { t.Parallel() stream := client.Sum(context.Background()) err := stream.Send(&pingv1.SumRequest{Number: 1}) assertNilOrEOF(t, err) _, err = stream.CloseAndReceive() assertErrorNoTrailers(t, err) }) t.Run("count_up", func(t *testing.T) { t.Parallel() stream, err := client.CountUp(context.Background(), connect.NewRequest(&pingv1.CountUpRequest{Number: 10})) assert.Nil(t, err) assert.False(t, stream.Receive()) assertErrorNoTrailers(t, stream.Err()) }) t.Run("cumsum", func(t *testing.T) { t.Parallel() stream := client.CumSum(context.Background()) assertNilOrEOF(t, stream.Send(&pingv1.CumSumRequest{Number: 10})) _, err := stream.Receive() assertErrorNoTrailers(t, err) assert.Nil(t, stream.CloseResponse()) }) t.Run("cumsum_empty_stream", func(t *testing.T) { t.Parallel() stream := client.CumSum(context.Background()) assert.Nil(t, stream.CloseRequest()) response, err := stream.Receive() assert.Nil(t, response) assertErrorNoTrailers(t, err) assert.Nil(t, stream.CloseResponse()) }) } func TestUnavailableIfHostInvalid(t *testing.T) { t.Parallel() client := pingv1connect.NewPingServiceClient( http.DefaultClient, "https://api.invalid/", ) _, err := client.Ping( context.Background(), connect.NewRequest(&pingv1.PingRequest{}), ) assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeUnavailable) } func TestBidiRequiresHTTP2(t *testing.T) { t.Parallel() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := io.WriteString(w, "hello world") assert.Nil(t, err) }) server := memhttptest.NewServer(t, handler) client := pingv1connect.NewPingServiceClient( &http.Client{Transport: server.TransportHTTP1()}, server.URL(), ) stream := client.CumSum(context.Background()) // Stream creates an async request, can error on Send or Receive. if err := stream.Send(&pingv1.CumSumRequest{}); err != nil { assert.ErrorIs(t, err, io.EOF) } assert.Nil(t, stream.CloseRequest()) _, err := stream.Receive() assert.NotNil(t, err) var connectErr *connect.Error assert.True(t, errors.As(err, &connectErr)) assert.Equal(t, connectErr.Code(), connect.CodeUnimplemented) assert.True( t, strings.HasSuffix(connectErr.Message(), ": bidi streams require at least HTTP/2"), ) } func TestCompressMinBytesClient(t *testing.T) { t.Parallel() assertContentType := func(tb testing.TB, text, expect string) { tb.Helper() mux := http.NewServeMux() mux.Handle("/", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { assert.Equal(tb, request.Header.Get("Content-Encoding"), expect) })) server := memhttptest.NewServer(t, mux) _, err := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), connect.WithSendGzip(), connect.WithCompressMinBytes(8), ).Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{Text: text})) assert.Nil(tb, err) } t.Run("request_uncompressed", func(t *testing.T) { t.Parallel() assertContentType(t, "ping", "") }) t.Run("request_compressed", func(t *testing.T) { t.Parallel() assertContentType(t, "pingping", "gzip") }) t.Run("request_uncompressed", func(t *testing.T) { t.Parallel() assertContentType(t, "ping", "") }) t.Run("request_compressed", func(t *testing.T) { t.Parallel() assertContentType(t, strings.Repeat("ping", 2), "gzip") }) } func TestCompressMinBytes(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler( pingServer{}, connect.WithCompressMinBytes(8), )) server := memhttptest.NewServer(t, mux) client := server.Client() getPingResponse := func(t *testing.T, pingText string) *http.Response { t.Helper() request := &pingv1.PingRequest{Text: pingText} requestBytes, err := proto.Marshal(request) assert.Nil(t, err) req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, server.URL()+pingv1connect.PingServicePingProcedure, bytes.NewReader(requestBytes), ) assert.Nil(t, err) req.Header.Set("Content-Type", "application/proto") response, err := client.Do(req) assert.Nil(t, err) t.Cleanup(func() { assert.Nil(t, response.Body.Close()) }) return response } t.Run("response_uncompressed", func(t *testing.T) { t.Parallel() assert.False(t, getPingResponse(t, "ping").Uncompressed) //nolint:bodyclose }) t.Run("response_compressed", func(t *testing.T) { t.Parallel() assert.True(t, getPingResponse(t, strings.Repeat("ping", 2)).Uncompressed) //nolint:bodyclose }) } func TestCustomCompression(t *testing.T) { t.Parallel() mux := http.NewServeMux() compressionName := "deflate" decompressor := func() connect.Decompressor { // Need to instantiate with a reader - before decompressing Reset(io.Reader) is called return newDeflateReader(strings.NewReader("")) } compressor := func() connect.Compressor { w, err := flate.NewWriter(&strings.Builder{}, flate.DefaultCompression) if err != nil { t.Fatalf("failed to create flate writer: %v", err) } return w } mux.Handle(pingv1connect.NewPingServiceHandler( pingServer{}, connect.WithCompression(compressionName, decompressor, compressor), )) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithAcceptCompression(compressionName, decompressor, compressor), connect.WithSendCompression(compressionName), ) request := &pingv1.PingRequest{Text: "testing 1..2..3.."} response, err := client.Ping(context.Background(), connect.NewRequest(request)) assert.Nil(t, err) assert.Equal(t, response.Msg, &pingv1.PingResponse{Text: request.GetText()}) } func TestClientWithoutGzipSupport(t *testing.T) { // See https://connectrpc.com/connect/pull/349 for why we want to // support this. TL;DR is that Microsoft's dapr sidecar can't handle // asymmetric compression. t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithAcceptCompression("gzip", nil, nil), connect.WithSendGzip(), ) request := &pingv1.PingRequest{Text: "gzip me!"} _, err := client.Ping(context.Background(), connect.NewRequest(request)) assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeUnknown) assert.True(t, strings.Contains(err.Error(), "unknown compression")) } func TestInvalidHeaderTimeout(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) server := memhttptest.NewServer(t, mux) getPingResponseWithTimeout := func(t *testing.T, timeout string) *http.Response { t.Helper() request, err := http.NewRequestWithContext( context.Background(), http.MethodPost, server.URL()+pingv1connect.PingServicePingProcedure, strings.NewReader("{}"), ) assert.Nil(t, err) request.Header.Set("Content-Type", "application/json") request.Header.Set("Connect-Timeout-Ms", timeout) response, err := server.Client().Do(request) assert.Nil(t, err) t.Cleanup(func() { assert.Nil(t, response.Body.Close()) }) return response } t.Run("timeout_non_numeric", func(t *testing.T) { t.Parallel() assert.Equal(t, getPingResponseWithTimeout(t, "10s").StatusCode, http.StatusBadRequest) //nolint:bodyclose }) t.Run("timeout_out_of_range", func(t *testing.T) { t.Parallel() assert.Equal(t, getPingResponseWithTimeout(t, "12345678901").StatusCode, http.StatusBadRequest) //nolint:bodyclose }) } func TestInterceptorReturnsWrongType(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) { if _, err := next(ctx, request); err != nil { return nil, err } return connect.NewResponse(&pingv1.CumSumResponse{ Sum: 1, }), nil } }))) _, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{Text: "hello!"})) assert.NotNil(t, err) var connectErr *connect.Error assert.True(t, errors.As(err, &connectErr)) assert.Equal(t, connectErr.Code(), connect.CodeInternal) assert.True(t, strings.Contains(connectErr.Message(), "unexpected client response type")) } func TestHandlerWithReadMaxBytes(t *testing.T) { t.Parallel() mux := http.NewServeMux() readMaxBytes := 1024 mux.Handle(pingv1connect.NewPingServiceHandler( pingServer{}, connect.WithConditionalHandlerOptions(func(spec connect.Spec) []connect.HandlerOption { var options []connect.HandlerOption if spec.Procedure == pingv1connect.PingServicePingProcedure { options = append(options, connect.WithReadMaxBytes(readMaxBytes)) } return options }), )) readMaxBytesMatrix := func(t *testing.T, client pingv1connect.PingServiceClient, compressed bool) { t.Helper() t.Run("equal_read_max", func(t *testing.T) { t.Parallel() // Serializes to exactly readMaxBytes (1024) - no errors expected pingRequest := &pingv1.PingRequest{Text: strings.Repeat("a", 1021)} assert.Equal(t, proto.Size(pingRequest), readMaxBytes) _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.Nil(t, err) }) t.Run("read_max_plus_one", func(t *testing.T) { t.Parallel() // Serializes to readMaxBytes+1 (1025) - expect invalid argument. // This will be over the limit after decompression but under with compression. pingRequest := &pingv1.PingRequest{Text: strings.Repeat("a", 1022)} if compressed { compressedSize := gzipCompressedSize(t, pingRequest) assert.True(t, compressedSize < readMaxBytes, assert.Sprintf("expected compressed size %d < %d", compressedSize, readMaxBytes)) } _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.NotNil(t, err, assert.Sprintf("expected non-nil error for large message")) assert.Equal(t, connect.CodeOf(err), connect.CodeResourceExhausted) assert.True(t, strings.HasSuffix(err.Error(), fmt.Sprintf("message size %d is larger than configured max %d", proto.Size(pingRequest), readMaxBytes))) }) t.Run("read_max_large", func(t *testing.T) { t.Parallel() if testing.Short() { t.Skipf("skipping %s test in short mode", t.Name()) } // Serializes to much larger than readMaxBytes (5 MiB) pingRequest := &pingv1.PingRequest{Text: strings.Repeat("abcde", 1024*1024)} expectedSize := proto.Size(pingRequest) // With gzip request compression, the error should indicate the envelope size (before decompression) is too large. if compressed { expectedSize = gzipCompressedSize(t, pingRequest) assert.True(t, expectedSize > readMaxBytes, assert.Sprintf("expected compressed size %d > %d", expectedSize, readMaxBytes)) } _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.NotNil(t, err, assert.Sprintf("expected non-nil error for large message")) assert.Equal(t, connect.CodeOf(err), connect.CodeResourceExhausted) assert.Equal(t, err.Error(), fmt.Sprintf("resource_exhausted: message size %d is larger than configured max %d", expectedSize, readMaxBytes)) }) } newHTTP2Server := func(t *testing.T) *memhttp.Server { t.Helper() server := memhttptest.NewServer(t, mux) return server } t.Run("connect", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) readMaxBytesMatrix(t, client, false) }) t.Run("connect_gzip", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithSendGzip()) readMaxBytesMatrix(t, client, true) }) t.Run("grpc", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPC()) readMaxBytesMatrix(t, client, false) }) t.Run("grpc_gzip", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPC(), connect.WithSendGzip()) readMaxBytesMatrix(t, client, true) }) t.Run("grpcweb", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPCWeb()) readMaxBytesMatrix(t, client, false) }) t.Run("grpcweb_gzip", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPCWeb(), connect.WithSendGzip()) readMaxBytesMatrix(t, client, true) }) } func TestHandlerWithHTTPMaxBytes(t *testing.T) { // This is similar to Connect's own ReadMaxBytes option, but applied to the // whole stream using the stdlib's http.MaxBytesHandler. t.Parallel() const readMaxBytes = 128 mux := http.NewServeMux() pingRoute, pingHandler := pingv1connect.NewPingServiceHandler(pingServer{}) mux.Handle(pingRoute, http.MaxBytesHandler(pingHandler, readMaxBytes)) run := func(t *testing.T, client pingv1connect.PingServiceClient, compressed bool) { t.Helper() t.Run("below_read_max", func(t *testing.T) { t.Parallel() _, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{})) assert.Nil(t, err) }) t.Run("just_above_max", func(t *testing.T) { t.Parallel() pingRequest := &pingv1.PingRequest{Text: strings.Repeat("a", readMaxBytes*10)} _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) if compressed { compressedSize := gzipCompressedSize(t, pingRequest) assert.True(t, compressedSize < readMaxBytes, assert.Sprintf("expected compressed size %d < %d", compressedSize, readMaxBytes)) assert.Nil(t, err) return } assert.NotNil(t, err, assert.Sprintf("expected non-nil error for large message")) assert.Equal(t, connect.CodeOf(err), connect.CodeResourceExhausted) }) t.Run("read_max_large", func(t *testing.T) { t.Parallel() if testing.Short() { t.Skipf("skipping %s test in short mode", t.Name()) } pingRequest := &pingv1.PingRequest{Text: strings.Repeat("abcde", 1024*1024)} if compressed { expectedSize := gzipCompressedSize(t, pingRequest) assert.True(t, expectedSize > readMaxBytes, assert.Sprintf("expected compressed size %d > %d", expectedSize, readMaxBytes)) } _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.NotNil(t, err, assert.Sprintf("expected non-nil error for large message")) assert.Equal(t, connect.CodeOf(err), connect.CodeResourceExhausted) }) } t.Run("connect", func(t *testing.T) { t.Parallel() server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) run(t, client, false) }) t.Run("connect_gzip", func(t *testing.T) { t.Parallel() server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithSendGzip()) run(t, client, true) }) t.Run("grpc", func(t *testing.T) { t.Parallel() server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPC()) run(t, client, false) }) t.Run("grpc_gzip", func(t *testing.T) { t.Parallel() server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPC(), connect.WithSendGzip()) run(t, client, true) }) t.Run("grpcweb", func(t *testing.T) { t.Parallel() server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPCWeb()) run(t, client, false) }) t.Run("grpcweb_gzip", func(t *testing.T) { t.Parallel() server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPCWeb(), connect.WithSendGzip()) run(t, client, true) }) } func TestClientWithReadMaxBytes(t *testing.T) { t.Parallel() createServer := func(tb testing.TB, enableCompression bool) *memhttp.Server { tb.Helper() mux := http.NewServeMux() var compressionOption connect.HandlerOption if enableCompression { compressionOption = connect.WithCompressMinBytes(1) } else { compressionOption = connect.WithCompressMinBytes(math.MaxInt) } mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{}, compressionOption)) server := memhttptest.NewServer(t, mux) return server } serverUncompressed := createServer(t, false) serverCompressed := createServer(t, true) readMaxBytes := 1024 readMaxBytesMatrix := func(t *testing.T, client pingv1connect.PingServiceClient, compressed bool) { t.Helper() t.Run("equal_read_max", func(t *testing.T) { t.Parallel() // Serializes to exactly readMaxBytes (1024) - no errors expected pingRequest := &pingv1.PingRequest{Text: strings.Repeat("a", 1021)} assert.Equal(t, proto.Size(pingRequest), readMaxBytes) _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.Nil(t, err) }) t.Run("read_max_plus_one", func(t *testing.T) { t.Parallel() // Serializes to readMaxBytes+1 (1025) - expect resource exhausted. // This will be over the limit after decompression but under with compression. pingRequest := &pingv1.PingRequest{Text: strings.Repeat("a", 1022)} _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.NotNil(t, err, assert.Sprintf("expected non-nil error for large message")) assert.Equal(t, connect.CodeOf(err), connect.CodeResourceExhausted) assert.True(t, strings.HasSuffix(err.Error(), fmt.Sprintf("message size %d is larger than configured max %d", proto.Size(pingRequest), readMaxBytes))) }) t.Run("read_max_large", func(t *testing.T) { t.Parallel() if testing.Short() { t.Skipf("skipping %s test in short mode", t.Name()) } // Serializes to much larger than readMaxBytes (5 MiB) pingRequest := &pingv1.PingRequest{Text: strings.Repeat("abcde", 1024*1024)} expectedSize := proto.Size(pingRequest) // With gzip response compression, the error should indicate the envelope size (before decompression) is too large. if compressed { expectedSize = gzipCompressedSize(t, pingRequest) assert.True(t, expectedSize > readMaxBytes, assert.Sprintf("expected compressed size %d > %d", expectedSize, readMaxBytes)) } assert.True(t, expectedSize > readMaxBytes, assert.Sprintf("expected compressed size %d > %d", expectedSize, readMaxBytes)) _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.NotNil(t, err, assert.Sprintf("expected non-nil error for large message")) assert.Equal(t, connect.CodeOf(err), connect.CodeResourceExhausted) assert.Equal(t, err.Error(), fmt.Sprintf("resource_exhausted: message size %d is larger than configured max %d", expectedSize, readMaxBytes)) }) } t.Run("connect", func(t *testing.T) { t.Parallel() client := pingv1connect.NewPingServiceClient(serverUncompressed.Client(), serverUncompressed.URL(), connect.WithReadMaxBytes(readMaxBytes)) readMaxBytesMatrix(t, client, false) }) t.Run("connect_gzip", func(t *testing.T) { t.Parallel() client := pingv1connect.NewPingServiceClient(serverCompressed.Client(), serverCompressed.URL(), connect.WithReadMaxBytes(readMaxBytes)) readMaxBytesMatrix(t, client, true) }) t.Run("grpc", func(t *testing.T) { t.Parallel() client := pingv1connect.NewPingServiceClient(serverUncompressed.Client(), serverUncompressed.URL(), connect.WithReadMaxBytes(readMaxBytes), connect.WithGRPC()) readMaxBytesMatrix(t, client, false) }) t.Run("grpc_gzip", func(t *testing.T) { t.Parallel() client := pingv1connect.NewPingServiceClient(serverCompressed.Client(), serverCompressed.URL(), connect.WithReadMaxBytes(readMaxBytes), connect.WithGRPC()) readMaxBytesMatrix(t, client, true) }) t.Run("grpcweb", func(t *testing.T) { t.Parallel() client := pingv1connect.NewPingServiceClient(serverUncompressed.Client(), serverUncompressed.URL(), connect.WithReadMaxBytes(readMaxBytes), connect.WithGRPCWeb()) readMaxBytesMatrix(t, client, false) }) t.Run("grpcweb_gzip", func(t *testing.T) { t.Parallel() client := pingv1connect.NewPingServiceClient(serverCompressed.Client(), serverCompressed.URL(), connect.WithReadMaxBytes(readMaxBytes), connect.WithGRPCWeb()) readMaxBytesMatrix(t, client, true) }) } func TestHandlerWithSendMaxBytes(t *testing.T) { t.Parallel() sendMaxBytes := 1024 sendMaxBytesMatrix := func(t *testing.T, client pingv1connect.PingServiceClient, compressed bool) { t.Helper() t.Run("equal_send_max", func(t *testing.T) { t.Parallel() // Serializes to exactly sendMaxBytes (1024) - no errors expected pingRequest := &pingv1.PingRequest{Text: strings.Repeat("a", 1021)} assert.Equal(t, proto.Size(pingRequest), sendMaxBytes) _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.Nil(t, err) }) t.Run("send_max_plus_one", func(t *testing.T) { t.Parallel() // Serializes to sendMaxBytes+1 (1025) - expect invalid argument. // This will be over the limit after decompression but under with compression. pingRequest := &pingv1.PingRequest{Text: strings.Repeat("a", 1022)} if compressed { compressedSize := gzipCompressedSize(t, pingRequest) assert.True(t, compressedSize < sendMaxBytes, assert.Sprintf("expected compressed size %d < %d", compressedSize, sendMaxBytes)) } _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) if compressed { assert.Nil(t, err) } else { assert.NotNil(t, err, assert.Sprintf("expected non-nil error for large message")) assert.Equal(t, connect.CodeOf(err), connect.CodeResourceExhausted) assert.True(t, strings.HasSuffix(err.Error(), fmt.Sprintf("message size %d exceeds sendMaxBytes %d", proto.Size(pingRequest), sendMaxBytes))) } }) t.Run("send_max_large", func(t *testing.T) { t.Parallel() if testing.Short() { t.Skipf("skipping %s test in short mode", t.Name()) } // Serializes to much larger than sendMaxBytes (5 MiB) pingRequest := &pingv1.PingRequest{Text: strings.Repeat("abcde", 1024*1024)} expectedSize := proto.Size(pingRequest) // With gzip request compression, the error should indicate the envelope size (before decompression) is too large. if compressed { expectedSize = gzipCompressedSize(t, pingRequest) assert.True(t, expectedSize > sendMaxBytes, assert.Sprintf("expected compressed size %d > %d", expectedSize, sendMaxBytes)) } _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.NotNil(t, err, assert.Sprintf("expected non-nil error for large message")) assert.Equal(t, connect.CodeOf(err), connect.CodeResourceExhausted) if compressed { assert.Equal(t, err.Error(), fmt.Sprintf("resource_exhausted: compressed message size %d exceeds sendMaxBytes %d", expectedSize, sendMaxBytes)) } else { assert.Equal(t, err.Error(), fmt.Sprintf("resource_exhausted: message size %d exceeds sendMaxBytes %d", expectedSize, sendMaxBytes)) } }) } newHTTP2Server := func(t *testing.T, compressed bool, sendMaxBytes int) *memhttp.Server { t.Helper() mux := http.NewServeMux() options := []connect.HandlerOption{connect.WithSendMaxBytes(sendMaxBytes)} if compressed { options = append(options, connect.WithCompressMinBytes(1)) } else { options = append(options, connect.WithCompressMinBytes(math.MaxInt)) } mux.Handle(pingv1connect.NewPingServiceHandler( pingServer{}, options..., )) server := memhttptest.NewServer(t, mux) return server } t.Run("connect", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t, false, sendMaxBytes) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) sendMaxBytesMatrix(t, client, false) }) t.Run("connect_gzip", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t, true, sendMaxBytes) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) sendMaxBytesMatrix(t, client, true) }) t.Run("grpc", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t, false, sendMaxBytes) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPC()) sendMaxBytesMatrix(t, client, false) }) t.Run("grpc_gzip", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t, true, sendMaxBytes) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPC()) sendMaxBytesMatrix(t, client, true) }) t.Run("grpcweb", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t, false, sendMaxBytes) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPCWeb()) sendMaxBytesMatrix(t, client, false) }) t.Run("grpcweb_gzip", func(t *testing.T) { t.Parallel() server := newHTTP2Server(t, true, sendMaxBytes) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPCWeb()) sendMaxBytesMatrix(t, client, true) }) } func TestClientWithSendMaxBytes(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) server := memhttptest.NewServer(t, mux) sendMaxBytesMatrix := func(t *testing.T, client pingv1connect.PingServiceClient, sendMaxBytes int, compressed bool) { t.Helper() t.Run("equal_send_max", func(t *testing.T) { t.Parallel() // Serializes to exactly sendMaxBytes (1024) - no errors expected pingRequest := &pingv1.PingRequest{Text: strings.Repeat("a", 1021)} assert.Equal(t, proto.Size(pingRequest), sendMaxBytes) _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.Nil(t, err) }) t.Run("send_max_plus_one", func(t *testing.T) { t.Parallel() // Serializes to sendMaxBytes+1 (1025) - expect resource exhausted. pingRequest := &pingv1.PingRequest{Text: strings.Repeat("a", 1022)} assert.Equal(t, proto.Size(pingRequest), sendMaxBytes+1) _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) if compressed { assert.True(t, gzipCompressedSize(t, pingRequest) < sendMaxBytes) assert.Nil(t, err, assert.Sprintf("expected nil error for compressed message < sendMaxBytes")) } else { assert.NotNil(t, err, assert.Sprintf("expected non-nil error for large message")) assert.Equal(t, connect.CodeOf(err), connect.CodeResourceExhausted) assert.True(t, strings.HasSuffix(err.Error(), fmt.Sprintf("message size %d exceeds sendMaxBytes %d", proto.Size(pingRequest), sendMaxBytes))) } }) t.Run("send_max_large", func(t *testing.T) { t.Parallel() if testing.Short() { t.Skipf("skipping %s test in short mode", t.Name()) } // Serializes to much larger than sendMaxBytes (5 MiB) pingRequest := &pingv1.PingRequest{Text: strings.Repeat("abcde", 1024*1024)} expectedSize := proto.Size(pingRequest) // With gzip response compression, the error should indicate the envelope size (before decompression) is too large. if compressed { expectedSize = gzipCompressedSize(t, pingRequest) } assert.True(t, expectedSize > sendMaxBytes) _, err := client.Ping(context.Background(), connect.NewRequest(pingRequest)) assert.NotNil(t, err, assert.Sprintf("expected non-nil error for large message")) assert.Equal(t, connect.CodeOf(err), connect.CodeResourceExhausted) if compressed { assert.Equal(t, err.Error(), fmt.Sprintf("resource_exhausted: compressed message size %d exceeds sendMaxBytes %d", expectedSize, sendMaxBytes)) } else { assert.Equal(t, err.Error(), fmt.Sprintf("resource_exhausted: message size %d exceeds sendMaxBytes %d", expectedSize, sendMaxBytes)) } }) } t.Run("connect", func(t *testing.T) { t.Parallel() sendMaxBytes := 1024 client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithSendMaxBytes(sendMaxBytes)) sendMaxBytesMatrix(t, client, sendMaxBytes, false) }) t.Run("connect_gzip", func(t *testing.T) { t.Parallel() sendMaxBytes := 1024 client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithSendMaxBytes(sendMaxBytes), connect.WithSendGzip()) sendMaxBytesMatrix(t, client, sendMaxBytes, true) }) t.Run("grpc", func(t *testing.T) { t.Parallel() sendMaxBytes := 1024 client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithSendMaxBytes(sendMaxBytes), connect.WithGRPC()) sendMaxBytesMatrix(t, client, sendMaxBytes, false) }) t.Run("grpc_gzip", func(t *testing.T) { t.Parallel() sendMaxBytes := 1024 client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithSendMaxBytes(sendMaxBytes), connect.WithGRPC(), connect.WithSendGzip()) sendMaxBytesMatrix(t, client, sendMaxBytes, true) }) t.Run("grpcweb", func(t *testing.T) { t.Parallel() sendMaxBytes := 1024 client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithSendMaxBytes(sendMaxBytes), connect.WithGRPCWeb()) sendMaxBytesMatrix(t, client, sendMaxBytes, false) }) t.Run("grpcweb_gzip", func(t *testing.T) { t.Parallel() sendMaxBytes := 1024 client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithSendMaxBytes(sendMaxBytes), connect.WithGRPCWeb(), connect.WithSendGzip()) sendMaxBytesMatrix(t, client, sendMaxBytes, true) }) } func TestBidiStreamServerSendsFirstMessage(t *testing.T) { t.Parallel() run := func(t *testing.T, opts ...connect.ClientOption) { t.Helper() headersSent := make(chan struct{}) pingServer := &pluggablePingServer{ cumSum: func(ctx context.Context, stream *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse]) error { close(headersSent) return nil }, } mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer)) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), connect.WithClientOptions(opts...), connect.WithInterceptors(&assertPeerInterceptor{t}), ) stream := client.CumSum(context.Background()) t.Cleanup(func() { assert.Nil(t, stream.CloseRequest()) assert.Nil(t, stream.CloseResponse()) }) assert.Nil(t, stream.Send(nil)) select { case <-time.After(time.Second): t.Error("timed out to get request headers") case <-headersSent: } } t.Run("connect", func(t *testing.T) { t.Parallel() run(t) }) t.Run("grpc", func(t *testing.T) { t.Parallel() run(t, connect.WithGRPC()) }) t.Run("grpcweb", func(t *testing.T) { t.Parallel() run(t, connect.WithGRPCWeb()) }) } func TestStreamForServer(t *testing.T) { t.Parallel() newPingClient := func(t *testing.T, pingServer pingv1connect.PingServiceHandler) pingv1connect.PingServiceClient { t.Helper() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer)) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), ) return client } t.Run("not-proto-message", func(t *testing.T) { t.Parallel() client := newPingClient(t, &pluggablePingServer{ cumSum: func(ctx context.Context, stream *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse]) error { return stream.Conn().Send("foobar") }, }) stream := client.CumSum(context.Background()) assert.Nil(t, stream.Send(nil)) _, err := stream.Receive() assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeInternal) assert.Nil(t, stream.CloseRequest()) }) t.Run("nil-message", func(t *testing.T) { t.Parallel() client := newPingClient(t, &pluggablePingServer{ cumSum: func(ctx context.Context, stream *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse]) error { return stream.Send(nil) }, }) stream := client.CumSum(context.Background()) assert.Nil(t, stream.Send(nil)) _, err := stream.Receive() assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeUnknown) assert.Nil(t, stream.CloseRequest()) }) t.Run("get-spec", func(t *testing.T) { t.Parallel() client := newPingClient(t, &pluggablePingServer{ cumSum: func(ctx context.Context, stream *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse]) error { assert.Equal(t, stream.Spec().StreamType, connect.StreamTypeBidi) assert.Equal(t, stream.Spec().Procedure, pingv1connect.PingServiceCumSumProcedure) assert.False(t, stream.Spec().IsClient) return nil }, }) stream := client.CumSum(context.Background()) assert.Nil(t, stream.Send(nil)) assert.Nil(t, stream.CloseRequest()) }) t.Run("server-stream", func(t *testing.T) { t.Parallel() client := newPingClient(t, &pluggablePingServer{ countUp: func(ctx context.Context, req *connect.Request[pingv1.CountUpRequest], stream *connect.ServerStream[pingv1.CountUpResponse]) error { assert.Equal(t, stream.Conn().Spec().StreamType, connect.StreamTypeServer) assert.Equal(t, stream.Conn().Spec().Procedure, pingv1connect.PingServiceCountUpProcedure) assert.False(t, stream.Conn().Spec().IsClient) assert.Nil(t, stream.Send(&pingv1.CountUpResponse{Number: 1})) return nil }, }) stream, err := client.CountUp(context.Background(), connect.NewRequest(&pingv1.CountUpRequest{})) assert.Nil(t, err) assert.NotNil(t, stream) assert.Nil(t, stream.Close()) }) t.Run("server-stream-send", func(t *testing.T) { t.Parallel() client := newPingClient(t, &pluggablePingServer{ countUp: func(ctx context.Context, req *connect.Request[pingv1.CountUpRequest], stream *connect.ServerStream[pingv1.CountUpResponse]) error { assert.Nil(t, stream.Send(&pingv1.CountUpResponse{Number: 1})) return nil }, }) stream, err := client.CountUp(context.Background(), connect.NewRequest(&pingv1.CountUpRequest{})) assert.Nil(t, err) assert.True(t, stream.Receive()) msg := stream.Msg() assert.NotNil(t, msg) assert.Equal(t, msg.GetNumber(), 1) assert.Nil(t, stream.Close()) }) t.Run("server-stream-send-nil", func(t *testing.T) { t.Parallel() client := newPingClient(t, &pluggablePingServer{ countUp: func(ctx context.Context, req *connect.Request[pingv1.CountUpRequest], stream *connect.ServerStream[pingv1.CountUpResponse]) error { stream.ResponseHeader().Set("foo", "bar") stream.ResponseTrailer().Set("bas", "blah") assert.Nil(t, stream.Send(nil)) return nil }, }) stream, err := client.CountUp(context.Background(), connect.NewRequest(&pingv1.CountUpRequest{})) assert.Nil(t, err) assert.False(t, stream.Receive()) headers := stream.ResponseHeader() assert.NotNil(t, headers) assert.Equal(t, headers.Get("foo"), "bar") trailers := stream.ResponseTrailer() assert.NotNil(t, trailers) assert.Equal(t, trailers.Get("bas"), "blah") assert.Nil(t, stream.Close()) }) t.Run("client-stream", func(t *testing.T) { t.Parallel() client := newPingClient(t, &pluggablePingServer{ sum: func(ctx context.Context, stream *connect.ClientStream[pingv1.SumRequest]) (*connect.Response[pingv1.SumResponse], error) { assert.Equal(t, stream.Spec().StreamType, connect.StreamTypeClient) assert.Equal(t, stream.Spec().Procedure, pingv1connect.PingServiceSumProcedure) assert.False(t, stream.Spec().IsClient) assert.True(t, stream.Receive()) msg := stream.Msg() assert.NotNil(t, msg) assert.Equal(t, msg.GetNumber(), 1) return connect.NewResponse(&pingv1.SumResponse{Sum: 1}), nil }, }) stream := client.Sum(context.Background()) assert.Nil(t, stream.Send(&pingv1.SumRequest{Number: 1})) res, err := stream.CloseAndReceive() assert.Nil(t, err) assert.NotNil(t, res) assert.Equal(t, res.Msg.GetSum(), 1) }) t.Run("client-stream-conn", func(t *testing.T) { t.Parallel() client := newPingClient(t, &pluggablePingServer{ sum: func(ctx context.Context, stream *connect.ClientStream[pingv1.SumRequest]) (*connect.Response[pingv1.SumResponse], error) { assert.True(t, stream.Receive()) assert.NotNil(t, stream.Conn().Send("not-proto")) return connect.NewResponse(&pingv1.SumResponse{}), nil }, }) stream := client.Sum(context.Background()) assert.Nil(t, stream.Send(&pingv1.SumRequest{Number: 1})) res, err := stream.CloseAndReceive() assert.Nil(t, err) assert.NotNil(t, res) }) t.Run("client-stream-send-msg", func(t *testing.T) { t.Parallel() client := newPingClient(t, &pluggablePingServer{ sum: func(ctx context.Context, stream *connect.ClientStream[pingv1.SumRequest]) (*connect.Response[pingv1.SumResponse], error) { assert.True(t, stream.Receive()) assert.Nil(t, stream.Conn().Send(&pingv1.SumResponse{Sum: 2})) return connect.NewResponse(&pingv1.SumResponse{}), nil }, }) stream := client.Sum(context.Background()) assert.Nil(t, stream.Send(&pingv1.SumRequest{Number: 1})) res, err := stream.CloseAndReceive() assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeUnknown) assert.Nil(t, res) }) } func TestConnectHTTPErrorCodes(t *testing.T) { t.Parallel() checkHTTPStatus := func(t *testing.T, connectCode connect.Code, wantHttpStatus int) { t.Helper() mux := http.NewServeMux() pluggableServer := &pluggablePingServer{ ping: func(_ context.Context, _ *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { return nil, connect.NewError(connectCode, errors.New("error")) }, } mux.Handle(pingv1connect.NewPingServiceHandler(pluggableServer)) server := memhttptest.NewServer(t, mux) req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, server.URL()+pingv1connect.PingServicePingProcedure, strings.NewReader("{}"), ) assert.Nil(t, err) req.Header.Set("Content-Type", "application/json") resp, err := server.Client().Do(req) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, wantHttpStatus, resp.StatusCode) connectClient := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) connectResp, err := connectClient.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{})) assert.NotNil(t, err) assert.Nil(t, connectResp) } t.Run("CodeCanceled-408", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeCanceled, 408) }) t.Run("CodeUnknown-500", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeUnknown, 500) }) t.Run("CodeInvalidArgument-400", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeInvalidArgument, 400) }) t.Run("CodeDeadlineExceeded-408", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeDeadlineExceeded, 408) }) t.Run("CodeNotFound-404", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeNotFound, 404) }) t.Run("CodeAlreadyExists-409", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeAlreadyExists, 409) }) t.Run("CodePermissionDenied-403", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodePermissionDenied, 403) }) t.Run("CodeResourceExhausted-429", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeResourceExhausted, 429) }) t.Run("CodeFailedPrecondition-412", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeFailedPrecondition, 412) }) t.Run("CodeAborted-409", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeAborted, 409) }) t.Run("CodeOutOfRange-400", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeOutOfRange, 400) }) t.Run("CodeUnimplemented-404", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeUnimplemented, 404) }) t.Run("CodeInternal-500", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeInternal, 500) }) t.Run("CodeUnavailable-503", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeUnavailable, 503) }) t.Run("CodeDataLoss-500", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeDataLoss, 500) }) t.Run("CodeUnauthenticated-401", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, connect.CodeUnauthenticated, 401) }) t.Run("100-500", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, 100, 500) }) t.Run("0-500", func(t *testing.T) { t.Parallel() checkHTTPStatus(t, 0, 500) }) } func TestFailCompression(t *testing.T) { t.Parallel() mux := http.NewServeMux() compressorName := "fail" compressor := func() connect.Compressor { return failCompressor{} } decompressor := func() connect.Decompressor { return failDecompressor{} } mux.Handle( pingv1connect.NewPingServiceHandler( pingServer{}, connect.WithCompression(compressorName, decompressor, compressor), ), ) server := memhttptest.NewServer(t, mux) pingclient := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), connect.WithAcceptCompression(compressorName, decompressor, compressor), connect.WithSendCompression(compressorName), ) _, err := pingclient.Ping( context.Background(), connect.NewRequest(&pingv1.PingRequest{ Text: "ping", }), ) assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeInternal) } func TestUnflushableResponseWriter(t *testing.T) { t.Parallel() assertIsFlusherErr := func(t *testing.T, err error) { t.Helper() assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeInternal, assert.Sprintf("got %v", err)) assert.True( t, strings.HasSuffix(err.Error(), "unflushableWriter does not implement http.Flusher"), assert.Sprintf("error doesn't reference http.Flusher: %s", err.Error()), ) } mux := http.NewServeMux() path, handler := pingv1connect.NewPingServiceHandler(pingServer{}) wrapped := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler.ServeHTTP(&unflushableWriter{w}, r) }) mux.Handle(path, wrapped) server := memhttptest.NewServer(t, mux) tests := []struct { name string options []connect.ClientOption }{ {"connect", nil}, {"grpc", []connect.ClientOption{connect.WithGRPC()}}, {"grpcweb", []connect.ClientOption{connect.WithGRPCWeb()}}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() pingclient := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), tt.options...) stream, err := pingclient.CountUp( context.Background(), connect.NewRequest(&pingv1.CountUpRequest{Number: 5}), ) if err != nil { assertIsFlusherErr(t, err) return } assert.False(t, stream.Receive()) assertIsFlusherErr(t, stream.Err()) }) } } func TestGRPCErrorMetadataIsTrailersOnly(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) server := memhttptest.NewServer(t, mux) protoBytes, err := proto.Marshal(&pingv1.FailRequest{Code: int32(connect.CodeInternal)}) assert.Nil(t, err) // Manually construct a gRPC prefix. Data is uncompressed, so the first byte // is 0. Set the last 4 bytes to the message length. var prefix [5]byte binary.BigEndian.PutUint32(prefix[1:5], uint32(len(protoBytes))) body := append(prefix[:], protoBytes...) // Manually send off a gRPC request. req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, server.URL()+pingv1connect.PingServiceFailProcedure, bytes.NewReader(body), ) assert.Nil(t, err) req.Header.Set("Content-Type", "application/grpc") res, err := server.Client().Do(req) assert.Nil(t, err) assert.Equal(t, res.StatusCode, http.StatusOK) assert.Equal(t, res.Header.Get("Content-Type"), "application/grpc") // pingServer.Fail adds handlerHeader and handlerTrailer to the error // metadata. The gRPC protocol should send all error metadata as trailers. assert.Zero(t, res.Header.Get(handlerHeader)) assert.Zero(t, res.Header.Get(handlerTrailer)) _, err = io.Copy(io.Discard, res.Body) assert.Nil(t, err) assert.Nil(t, res.Body.Close()) assert.NotZero(t, res.Trailer.Get(handlerHeader)) assert.NotZero(t, res.Trailer.Get(handlerTrailer)) } func TestConnectProtocolHeaderSentByDefault(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{}, connect.WithRequireConnectProtocolHeader())) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) _, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{})) assert.Nil(t, err) stream := client.CumSum(context.Background()) assert.Nil(t, stream.Send(&pingv1.CumSumRequest{})) _, err = stream.Receive() assert.Nil(t, err) assert.Nil(t, stream.CloseRequest()) assert.Nil(t, stream.CloseResponse()) } func TestConnectProtocolHeaderRequired(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler( pingServer{}, connect.WithRequireConnectProtocolHeader(), )) server := memhttptest.NewServer(t, mux) tests := []struct { headers http.Header }{ {http.Header{}}, {http.Header{"Connect-Protocol-Version": []string{"0"}}}, } for _, tcase := range tests { req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, server.URL()+pingv1connect.PingServicePingProcedure, strings.NewReader("{}"), ) assert.Nil(t, err) req.Header.Set("Content-Type", "application/json") for k, v := range tcase.headers { req.Header[k] = v } response, err := server.Client().Do(req) assert.Nil(t, err) assert.Nil(t, response.Body.Close()) assert.Equal(t, response.StatusCode, http.StatusBadRequest) } } func TestAllowCustomUserAgent(t *testing.T) { t.Parallel() const customAgent = "custom" mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(&pluggablePingServer{ ping: func(_ context.Context, req *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { agent := req.Header().Get("User-Agent") assert.Equal(t, agent, customAgent) return connect.NewResponse(&pingv1.PingResponse{Number: req.Msg.GetNumber()}), nil }, })) server := memhttptest.NewServer(t, mux) // If the user has set a User-Agent, we shouldn't clobber it. tests := []struct { protocol string opts []connect.ClientOption }{ {"connect", nil}, {"grpc", []connect.ClientOption{connect.WithGRPC()}}, {"grpcweb", []connect.ClientOption{connect.WithGRPCWeb()}}, } for _, testCase := range tests { client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), testCase.opts...) req := connect.NewRequest(&pingv1.PingRequest{Number: 42}) req.Header().Set("User-Agent", customAgent) _, err := client.Ping(context.Background(), req) assert.Nil(t, err) } } func TestWebXUserAgent(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(&pluggablePingServer{ ping: func(_ context.Context, req *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { agent := req.Header().Get("User-Agent") assert.NotZero(t, agent) assert.Equal( t, req.Header().Get("X-User-Agent"), agent, ) return connect.NewResponse(&pingv1.PingResponse{Number: req.Msg.GetNumber()}), nil }, })) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithGRPCWeb()) req := connect.NewRequest(&pingv1.PingRequest{Number: 42}) _, err := client.Ping(context.Background(), req) assert.Nil(t, err) } func TestBidiOverHTTP1(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) server := memhttptest.NewServer(t, mux) // Clients expecting a full-duplex connection that end up with a simplex // HTTP/1.1 connection shouldn't hang. Instead, the server should close the // TCP connection. client := pingv1connect.NewPingServiceClient( &http.Client{Transport: server.TransportHTTP1()}, server.URL(), ) stream := client.CumSum(context.Background()) // Stream creates an async request, can error on Send or Receive. if err := stream.Send(&pingv1.CumSumRequest{Number: 2}); err != nil { assert.ErrorIs(t, err, io.EOF) } _, err := stream.Receive() assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeUnknown) assert.Equal(t, err.Error(), "unknown: HTTP status 505 HTTP Version Not Supported") assert.Nil(t, stream.CloseRequest()) assert.Nil(t, stream.CloseResponse()) } func TestHandlerReturnsNilResponse(t *testing.T) { // When user-written handlers return nil responses _and_ nil errors, ensure // that the resulting panic includes at least the name of the procedure. t.Parallel() var panics int recoverPanic := func(_ context.Context, spec connect.Spec, _ http.Header, p any) error { panics++ assert.NotNil(t, p) str := fmt.Sprint(p) assert.True( t, strings.Contains(str, spec.Procedure), assert.Sprintf("%q does not contain procedure %q", str, spec.Procedure), ) return connect.NewError(connect.CodeInternal, errors.New(str)) } mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(&pluggablePingServer{ ping: func(ctx context.Context, req *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { return nil, nil //nolint: nilnil }, sum: func(ctx context.Context, req *connect.ClientStream[pingv1.SumRequest]) (*connect.Response[pingv1.SumResponse], error) { return nil, nil //nolint: nilnil }, }, connect.WithRecover(recoverPanic))) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) _, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{})) assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeInternal) _, err = client.Sum(context.Background()).CloseAndReceive() assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeInternal) assert.Equal(t, panics, 2) } func TestStreamUnexpectedEOF(t *testing.T) { t.Parallel() // Initialized by the test case. testcaseMux := make(map[string]http.HandlerFunc) mux := http.NewServeMux() mux.HandleFunc("/", func(responseWriter http.ResponseWriter, request *http.Request) { testcase, ok := testcaseMux[request.Header.Get("Test-Case")] if !ok { responseWriter.WriteHeader(http.StatusNotFound) return } _, _ = io.Copy(io.Discard, request.Body) testcase(responseWriter, request) }) server := memhttptest.NewServer(t, mux) head := [5]byte{} payload := []byte(`{"number": 42}`) binary.BigEndian.PutUint32(head[1:], uint32(len(payload))) testcases := []struct { name string handler http.HandlerFunc options []connect.ClientOption expectCode connect.Code expectMsg string }{{ name: "connect_missing_end", options: []connect.ClientOption{connect.WithProtoJSON()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { header := responseWriter.Header() header.Set("Content-Type", "application/connect+json") _, err := responseWriter.Write(head[:]) assert.Nil(t, err) _, err = responseWriter.Write(payload) assert.Nil(t, err) }, expectCode: connect.CodeInternal, expectMsg: "internal: protocol error: unexpected EOF", }, { name: "grpc_missing_end", options: []connect.ClientOption{connect.WithProtoJSON(), connect.WithGRPC()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { header := responseWriter.Header() header.Set("Content-Type", "application/grpc+json") _, err := responseWriter.Write(head[:]) assert.Nil(t, err) _, err = responseWriter.Write(payload) assert.Nil(t, err) }, expectCode: connect.CodeInternal, expectMsg: "internal: protocol error: no Grpc-Status trailer: unexpected EOF", }, { name: "grpc-web_missing_end", options: []connect.ClientOption{connect.WithProtoJSON(), connect.WithGRPCWeb()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { header := responseWriter.Header() header.Set("Content-Type", "application/grpc-web+json") _, err := responseWriter.Write(head[:]) assert.Nil(t, err) _, _ = responseWriter.Write(payload) assert.Nil(t, err) }, expectCode: connect.CodeInternal, expectMsg: "internal: protocol error: no Grpc-Status trailer: unexpected EOF", }, { name: "connect_partial_payload", options: []connect.ClientOption{connect.WithProtoJSON()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { header := responseWriter.Header() header.Set("Content-Type", "application/connect+json") _, err := responseWriter.Write(head[:]) assert.Nil(t, err) _, err = responseWriter.Write(payload[:len(payload)-1]) assert.Nil(t, err) }, expectCode: connect.CodeInvalidArgument, expectMsg: fmt.Sprintf("invalid_argument: protocol error: promised %d bytes in enveloped message, got %d bytes", len(payload), len(payload)-1), }, { name: "grpc_partial_payload", options: []connect.ClientOption{connect.WithProtoJSON(), connect.WithGRPC()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { header := responseWriter.Header() header.Set("Content-Type", "application/grpc+json") _, err := responseWriter.Write(head[:]) assert.Nil(t, err) _, err = responseWriter.Write(payload[:len(payload)-1]) assert.Nil(t, err) }, expectCode: connect.CodeInvalidArgument, expectMsg: fmt.Sprintf("invalid_argument: protocol error: promised %d bytes in enveloped message, got %d bytes", len(payload), len(payload)-1), }, { name: "grpc-web_partial_payload", options: []connect.ClientOption{connect.WithProtoJSON(), connect.WithGRPCWeb()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { header := responseWriter.Header() header.Set("Content-Type", "application/grpc-web+json") _, err := responseWriter.Write(head[:]) assert.Nil(t, err) _, err = responseWriter.Write(payload[:len(payload)-1]) assert.Nil(t, err) }, expectCode: connect.CodeInvalidArgument, expectMsg: fmt.Sprintf("invalid_argument: protocol error: promised %d bytes in enveloped message, got %d bytes", len(payload), len(payload)-1), }, { name: "connect_partial_frame", options: []connect.ClientOption{connect.WithProtoJSON()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { header := responseWriter.Header() header.Set("Content-Type", "application/connect+json") _, err := responseWriter.Write(head[:4]) assert.Nil(t, err) }, expectCode: connect.CodeInvalidArgument, expectMsg: "invalid_argument: protocol error: incomplete envelope: unexpected EOF", }, { name: "grpc_partial_frame", options: []connect.ClientOption{connect.WithProtoJSON(), connect.WithGRPC()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { header := responseWriter.Header() header.Set("Content-Type", "application/grpc+json") _, err := responseWriter.Write(head[:4]) assert.Nil(t, err) }, expectCode: connect.CodeInvalidArgument, expectMsg: "invalid_argument: protocol error: incomplete envelope: unexpected EOF", }, { name: "grpc-web_partial_frame", options: []connect.ClientOption{connect.WithProtoJSON(), connect.WithGRPCWeb()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { header := responseWriter.Header() header.Set("Content-Type", "application/grpc-web+json") _, err := responseWriter.Write(head[:4]) assert.Nil(t, err) }, expectCode: connect.CodeInvalidArgument, expectMsg: "invalid_argument: protocol error: incomplete envelope: unexpected EOF", }, { name: "connect_excess_eof", options: []connect.ClientOption{connect.WithProtoJSON()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { _, err := responseWriter.Write(head[:]) assert.Nil(t, err) _, err = responseWriter.Write(payload) assert.Nil(t, err) // Write EOF _, err = responseWriter.Write([]byte{1 << 1, 0, 0, 0, 2}) assert.Nil(t, err) _, err = responseWriter.Write([]byte("{}")) assert.Nil(t, err) // Excess payload _, err = responseWriter.Write(head[:]) assert.Nil(t, err) _, err = responseWriter.Write(payload) assert.Nil(t, err) }, expectCode: connect.CodeInternal, expectMsg: fmt.Sprintf("internal: corrupt response: %d extra bytes after end of stream", len(payload)+len(head)), }, { name: "grpc-web_excess_eof", options: []connect.ClientOption{connect.WithProtoJSON(), connect.WithGRPCWeb()}, handler: func(responseWriter http.ResponseWriter, _ *http.Request) { _, err := responseWriter.Write(head[:]) assert.Nil(t, err) _, err = responseWriter.Write(payload) assert.Nil(t, err) // Write EOF var buf bytes.Buffer trailer := http.Header{"grpc-status": []string{"0"}} assert.Nil(t, trailer.Write(&buf)) var head [5]byte head[0] = 1 << 7 binary.BigEndian.PutUint32(head[1:], uint32(buf.Len())) _, err = responseWriter.Write(head[:]) assert.Nil(t, err) _, err = responseWriter.Write(buf.Bytes()) assert.Nil(t, err) // Excess payload _, err = responseWriter.Write(head[:]) assert.Nil(t, err) _, err = responseWriter.Write(payload) assert.Nil(t, err) }, expectCode: connect.CodeInternal, expectMsg: fmt.Sprintf("internal: corrupt response: %d extra bytes after end of stream", len(payload)+len(head)), }} for _, testcase := range testcases { testcaseMux[t.Name()+"/"+testcase.name] = testcase.handler } for _, testcase := range testcases { testcase := testcase t.Run(testcase.name, func(t *testing.T) { t.Parallel() client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), testcase.options..., ) const upTo = 2 request := connect.NewRequest(&pingv1.CountUpRequest{Number: upTo}) request.Header().Set("Test-Case", t.Name()) stream, err := client.CountUp(context.Background(), request) assert.Nil(t, err) for i := 0; stream.Receive() && i < upTo; i++ { assert.Equal(t, stream.Msg().GetNumber(), 42) } assert.NotNil(t, stream.Err()) assert.Equal(t, connect.CodeOf(stream.Err()), testcase.expectCode) assert.Equal(t, stream.Err().Error(), testcase.expectMsg) }) } } // TestBlankImportCodeGeneration tests that services.connect.go is generated with // blank import statements to services.pb.go so that the service's Descriptor is // available in the global proto registry. func TestBlankImportCodeGeneration(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName(importv1connect.ImportServiceName) assert.Nil(t, err) assert.NotNil(t, desc) } type unflushableWriter struct { w http.ResponseWriter } func (w *unflushableWriter) Header() http.Header { return w.w.Header() } func (w *unflushableWriter) Write(b []byte) (int, error) { return w.w.Write(b) } func (w *unflushableWriter) WriteHeader(code int) { w.w.WriteHeader(code) } func gzipCompressedSize(tb testing.TB, message proto.Message) int { tb.Helper() uncompressed, err := proto.Marshal(message) assert.Nil(tb, err) var buf bytes.Buffer gzipWriter := gzip.NewWriter(&buf) _, err = gzipWriter.Write(uncompressed) assert.Nil(tb, err) assert.Nil(tb, gzipWriter.Close()) return buf.Len() } type failCodec struct{} func (c failCodec) Name() string { return "proto" } func (c failCodec) Marshal(message any) ([]byte, error) { return nil, errors.New("boom") } func (c failCodec) Unmarshal(data []byte, message any) error { protoMessage, ok := message.(proto.Message) if !ok { return fmt.Errorf("not protobuf: %T", message) } return proto.Unmarshal(data, protoMessage) } type pluggablePingServer struct { pingv1connect.UnimplementedPingServiceHandler ping func(context.Context, *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) sum func(context.Context, *connect.ClientStream[pingv1.SumRequest]) (*connect.Response[pingv1.SumResponse], error) countUp func(context.Context, *connect.Request[pingv1.CountUpRequest], *connect.ServerStream[pingv1.CountUpResponse]) error cumSum func(context.Context, *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse]) error } func (p *pluggablePingServer) Ping( ctx context.Context, request *connect.Request[pingv1.PingRequest], ) (*connect.Response[pingv1.PingResponse], error) { return p.ping(ctx, request) } func (p *pluggablePingServer) Sum( ctx context.Context, stream *connect.ClientStream[pingv1.SumRequest], ) (*connect.Response[pingv1.SumResponse], error) { return p.sum(ctx, stream) } func (p *pluggablePingServer) CountUp( ctx context.Context, req *connect.Request[pingv1.CountUpRequest], stream *connect.ServerStream[pingv1.CountUpResponse], ) error { return p.countUp(ctx, req, stream) } func (p *pluggablePingServer) CumSum( ctx context.Context, stream *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse], ) error { return p.cumSum(ctx, stream) } func failNoHTTP2(tb testing.TB, stream *connect.BidiStreamForClient[pingv1.CumSumRequest, pingv1.CumSumResponse]) { tb.Helper() if err := stream.Send(&pingv1.CumSumRequest{}); err != nil { assert.ErrorIs(tb, err, io.EOF) assert.Equal(tb, connect.CodeOf(err), connect.CodeUnknown) } assert.Nil(tb, stream.CloseRequest()) _, err := stream.Receive() assert.NotNil(tb, err) // should be 505 assert.True( tb, strings.Contains(err.Error(), "HTTP status 505"), assert.Sprintf("expected 505, got %v", err), ) assert.Nil(tb, stream.CloseResponse()) } func expectClientHeader(check bool, req connect.AnyRequest) error { if !check { return nil } return expectMetadata(req.Header(), "header", clientHeader, headerValue) } func expectMetadata(meta http.Header, metaType, key, value string) error { if got := meta.Get(key); got != value { return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf( "%s %q: got %q, expected %q", metaType, key, got, value, )) } return nil } type pingServer struct { pingv1connect.UnimplementedPingServiceHandler checkMetadata bool } func (p pingServer) Ping(ctx context.Context, request *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { if err := expectClientHeader(p.checkMetadata, request); err != nil { return nil, err } if request.Peer().Addr == "" { return nil, connect.NewError(connect.CodeInternal, errors.New("no peer address")) } if request.Peer().Protocol == "" { return nil, connect.NewError(connect.CodeInternal, errors.New("no peer protocol")) } response := connect.NewResponse( &pingv1.PingResponse{ Number: request.Msg.GetNumber(), Text: request.Msg.GetText(), }, ) response.Header().Set(handlerHeader, headerValue) response.Trailer().Set(handlerTrailer, trailerValue) return response, nil } func (p pingServer) Fail(ctx context.Context, request *connect.Request[pingv1.FailRequest]) (*connect.Response[pingv1.FailResponse], error) { if err := expectClientHeader(p.checkMetadata, request); err != nil { return nil, err } if request.Peer().Addr == "" { return nil, connect.NewError(connect.CodeInternal, errors.New("no peer address")) } if request.Peer().Protocol == "" { return nil, connect.NewError(connect.CodeInternal, errors.New("no peer protocol")) } err := connect.NewError(connect.Code(request.Msg.GetCode()), errors.New(errorMessage)) err.Meta().Set(handlerHeader, headerValue) err.Meta().Set(handlerTrailer, trailerValue) return nil, err } func (p pingServer) Sum( ctx context.Context, stream *connect.ClientStream[pingv1.SumRequest], ) (*connect.Response[pingv1.SumResponse], error) { if p.checkMetadata { if err := expectMetadata(stream.RequestHeader(), "header", clientHeader, headerValue); err != nil { return nil, err } } if stream.Peer().Addr == "" { return nil, connect.NewError(connect.CodeInternal, errors.New("no peer address")) } if stream.Peer().Protocol == "" { return nil, connect.NewError(connect.CodeInternal, errors.New("no peer protocol")) } var sum int64 for stream.Receive() { sum += stream.Msg().GetNumber() } if stream.Err() != nil { return nil, stream.Err() } response := connect.NewResponse(&pingv1.SumResponse{Sum: sum}) response.Header().Set(handlerHeader, headerValue) response.Trailer().Set(handlerTrailer, trailerValue) return response, nil } func (p pingServer) CountUp( ctx context.Context, request *connect.Request[pingv1.CountUpRequest], stream *connect.ServerStream[pingv1.CountUpResponse], ) error { if err := expectClientHeader(p.checkMetadata, request); err != nil { return err } if request.Peer().Addr == "" { return connect.NewError(connect.CodeInternal, errors.New("no peer address")) } if request.Peer().Protocol == "" { return connect.NewError(connect.CodeInternal, errors.New("no peer protocol")) } if request.Msg.GetNumber() <= 0 { return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf( "number must be positive: got %v", request.Msg.GetNumber(), )) } stream.ResponseHeader().Set(handlerHeader, headerValue) stream.ResponseTrailer().Set(handlerTrailer, trailerValue) for i := int64(1); i <= request.Msg.GetNumber(); i++ { if err := stream.Send(&pingv1.CountUpResponse{Number: i}); err != nil { return err } } return nil } func (p pingServer) CumSum( ctx context.Context, stream *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse], ) error { var sum int64 if p.checkMetadata { if err := expectMetadata(stream.RequestHeader(), "header", clientHeader, headerValue); err != nil { return err } } if stream.Peer().Addr == "" { return connect.NewError(connect.CodeInternal, errors.New("no peer address")) } if stream.Peer().Protocol == "" { return connect.NewError(connect.CodeInternal, errors.New("no peer address")) } stream.ResponseHeader().Set(handlerHeader, headerValue) stream.ResponseTrailer().Set(handlerTrailer, trailerValue) for { msg, err := stream.Receive() if errors.Is(err, io.EOF) { return nil } else if err != nil { return err } sum += msg.GetNumber() if err := stream.Send(&pingv1.CumSumResponse{Sum: sum}); err != nil { return err } } } type deflateReader struct { r io.ReadCloser } func newDeflateReader(r io.Reader) *deflateReader { return &deflateReader{r: flate.NewReader(r)} } func (d *deflateReader) Read(p []byte) (int, error) { return d.r.Read(p) } func (d *deflateReader) Close() error { return d.r.Close() } func (d *deflateReader) Reset(reader io.Reader) error { if resetter, ok := d.r.(flate.Resetter); ok { return resetter.Reset(reader, nil) } return fmt.Errorf("flate reader should implement flate.Resetter") } var _ connect.Decompressor = (*deflateReader)(nil) type trimTrailerWriter struct { w http.ResponseWriter } func (l *trimTrailerWriter) Header() http.Header { return l.w.Header() } // Write writes b to underlying writer and counts written size. func (l *trimTrailerWriter) Write(b []byte) (int, error) { l.removeTrailers() return l.w.Write(b) } // WriteHeader writes s to underlying writer and retains the status. func (l *trimTrailerWriter) WriteHeader(s int) { l.removeTrailers() l.w.WriteHeader(s) } // Flush implements http.Flusher. func (l *trimTrailerWriter) Flush() { l.removeTrailers() if f, ok := l.w.(http.Flusher); ok { f.Flush() } } func (l *trimTrailerWriter) removeTrailers() { for _, v := range l.w.Header().Values("Trailer") { l.w.Header().Del(v) } l.w.Header().Del("Trailer") for k := range l.w.Header() { if strings.HasPrefix(k, http.TrailerPrefix) { l.w.Header().Del(k) } } } func newHTTPMiddlewareError() *connect.Error { err := connect.NewError(connect.CodeResourceExhausted, errors.New("error from HTTP middleware")) err.Meta().Set("Middleware-Foo", "bar") return err } type failDecompressor struct { connect.Decompressor } type failCompressor struct{} func (failCompressor) Write([]byte) (int, error) { return 0, errors.New("failCompressor") } func (failCompressor) Close() error { return errors.New("failCompressor") } func (failCompressor) Reset(io.Writer) {} connect-go-1.13.0/duplex_http_call.go000066400000000000000000000261571453471351600175360ustar00rootroot00000000000000// Copyright 2021-2023 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" "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 // We'll use a pipe as the request body. We hand the read side of the pipe to // net/http, and we write to the write side (naturally). The two ends are // safe to use concurrently. requestBodyReader *io.PipeReader 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) pipeReader, pipeWriter := io.Pipe() // 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: pipeReader, Host: url.Host, }).WithContext(ctx) return &duplexHTTPCall{ ctx: ctx, httpClient: httpClient, streamType: spec.StreamType, requestBodyReader: pipeReader, requestBodyWriter: pipeWriter, request: request, responseReady: make(chan struct{}), } } // Send sends a message to the server. func (d *duplexHTTPCall) Send(payload messsagePayload) (int64, error) { isFirst := d.ensureRequestMade() // Before we send any data, check if the context has been canceled. 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. return bytesWritten, io.EOF } return bytesWritten, err } // Close 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. d.ensureRequestMade() // 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. return d.requestBodyWriter.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 } // 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) } if d.response == nil { return 0, fmt.Errorf("nil response from %v", d.request.URL) } n, err := d.response.Body.Read(data) return n, wrapIfRSTError(err) } func (d *duplexHTTPCall) CloseRead() error { _ = d.BlockUntilResponseReady() if d.response == nil { return nil } _, err := discard(d.response.Body) closeErr := d.response.Body.Close() if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { err = closeErr } err = wrapIfContextError(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 } // ensureRequestMade sends the request headers and starts the response stream. // It is not safe to call this concurrently. Write and CloseWrite call this but // ensure that they're not called concurrently. func (d *duplexHTTPCall) ensureRequestMade() (isFirst bool) { if d.requestSent.CompareAndSwap(false, true) { go d.makeRequest() return true } return false } 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 := d.request.Header.Get(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 { 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.requestBodyWriter.Close() 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.requestBodyWriter.Close() 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.requestBodyWriter.Close() } } // messsagePayload is a sized and seekable message payload. The interface is // implemented by [*bytes.Reader] and *envelope. type messsagePayload 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 _ messsagePayload = 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(messsagePayload) (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 messsagePayload) (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 } connect-go-1.13.0/envelope.go000066400000000000000000000266151453471351600160170ustar00rootroot00000000000000// Copyright 2021-2023 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/binary" "errors" "io" ) // 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 _ messsagePayload = (*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 := makeEnvelopePrefix(e.Flags, e.Data.Len()) 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 := makeEnvelopePrefix(e.Flags, e.Data.Len()) 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 { 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 = wrapIfContextError(err) if connectErr, ok := asError(err); ok { return connectErr } return errorf(CodeUnknown, "write envelope: %w", err) } return nil } type envelopeReader struct { reader io.Reader codec Codec last envelope compressionPool *compressionPool bufferPool *bufferPool readMaxBytes int } func (r *envelopeReader) Unmarshal(message any) *Error { buffer := r.bufferPool.Get() defer r.bufferPool.Put(buffer) env := &envelope{Data: buffer} err := r.Read(env) switch { 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) { if r.compressionPool == nil { return errorf( CodeInvalidArgument, "protocol error: sent compressed message without compression support", ) } decompressed := r.bufferPool.Get() defer 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. if numBytes, err := discard(r.reader); 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. Since we've deferred functions to return env's // underlying buffer to a pool, we need to keep a copy. copiedData := make([]byte, data.Len()) copy(copiedData, data.Bytes()) r.last = envelope{ Data: bytes.NewBuffer(copiedData), Flags: env.Flags, } 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. if _, err := io.ReadFull(r.reader, prefixes[:]); 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 = wrapIfContextError(err) if connectErr, ok := asError(err); ok { return connectErr } // Something else has gone wrong - the stream didn't end cleanly. if connectErr, ok := asError(err); ok { return connectErr } if maxBytesErr := asMaxBytesError(err, "read 5 byte message prefix"); maxBytesErr != nil { // We're reading from an http.MaxBytesHandler, and we've exceeded the read limit. return maxBytesErr } 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) { _, err := io.CopyN(io.Discard, r.reader, size) 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. if readN, err := io.CopyN(env.Data, r.reader, size); err != nil { if maxBytesErr := asMaxBytesError(err, "read %d byte message", size); maxBytesErr != nil { // We're reading from an http.MaxBytesHandler, and we've exceeded the read limit. return maxBytesErr } 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 = wrapIfContextError(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 { prefix := [5]byte{} prefix[0] = flags binary.BigEndian.PutUint32(prefix[1:5], uint32(size)) return prefix } connect-go-1.13.0/envelope_test.go000066400000000000000000000061041453471351600170450ustar00rootroot00000000000000// Copyright 2021-2023 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" "io" "testing" "connectrpc.com/connect/internal/assert" ) func TestEnvelope(t *testing.T) { t.Parallel() payload := []byte(`{"number": 42}`) head := makeEnvelopePrefix(0, len(payload)) buf := &bytes.Buffer{} buf.Write(head[:]) buf.Write(payload) t.Run("read", func(t *testing.T) { t.Parallel() t.Run("full", func(t *testing.T) { t.Parallel() env := &envelope{Data: &bytes.Buffer{}} rdr := envelopeReader{ reader: bytes.NewReader(buf.Bytes()), } assert.Nil(t, rdr.Read(env)) assert.Equal(t, payload, env.Data.Bytes()) }) t.Run("byteByByte", func(t *testing.T) { t.Parallel() env := &envelope{Data: &bytes.Buffer{}} rdr := envelopeReader{ reader: byteByByteReader{ reader: bytes.NewReader(buf.Bytes()), }, } assert.Nil(t, rdr.Read(env)) assert.Equal(t, payload, env.Data.Bytes()) }) }) t.Run("write", func(t *testing.T) { t.Parallel() t.Run("full", func(t *testing.T) { t.Parallel() dst := &bytes.Buffer{} wtr := envelopeWriter{ sender: writeSender{writer: dst}, } env := &envelope{Data: bytes.NewBuffer(payload)} err := wtr.Write(env) assert.Nil(t, err) assert.Equal(t, buf.Bytes(), dst.Bytes()) }) t.Run("partial", func(t *testing.T) { t.Parallel() dst := &bytes.Buffer{} env := &envelope{Data: bytes.NewBuffer(payload)} _, err := io.CopyN(dst, env, 2) assert.Nil(t, err) _, err = env.WriteTo(dst) assert.Nil(t, err) assert.Equal(t, buf.Bytes(), dst.Bytes()) }) }) t.Run("seek", func(t *testing.T) { t.Parallel() t.Run("start", func(t *testing.T) { t.Parallel() dst1 := &bytes.Buffer{} dst2 := &bytes.Buffer{} env := &envelope{Data: bytes.NewBuffer(payload)} _, err := io.CopyN(dst1, env, 2) assert.Nil(t, err) assert.Equal(t, env.Len(), len(payload)+3) _, err = env.Seek(0, io.SeekStart) assert.Nil(t, err) assert.Equal(t, env.Len(), len(payload)+5) _, err = io.CopyN(dst2, env, 2) assert.Nil(t, err) assert.Equal(t, dst1.Bytes(), dst2.Bytes()) _, err = env.WriteTo(dst2) assert.Nil(t, err) assert.Equal(t, dst2.Bytes(), buf.Bytes()) assert.Equal(t, env.Len(), 0) }) }) } // byteByByteReader is test reader that reads a single byte at a time. type byteByByteReader struct { reader io.ByteReader } func (b byteByByteReader) Read(data []byte) (int, error) { if len(data) == 0 { return 0, nil } next, err := b.reader.ReadByte() if err != nil { return 0, err } data[0] = next return 1, nil } connect-go-1.13.0/error.go000066400000000000000000000357671453471351600153430ustar00rootroot00000000000000// Copyright 2021-2023 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 { pb *anypb.Any 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{pb: pb}, nil } pb, err := anypb.New(msg) if err != nil { return nil, err } return &ErrorDetail{pb: pb}, 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 typeNameFromURL(d.pb.GetTypeUrl()) } // Bytes returns a copy of the Protobuf-serialized detail. func (d *ErrorDetail) Bytes() []byte { out := make([]byte, len(d.pb.GetValue())) copy(out, d.pb.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) { return d.pb.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 // OpenTelemtetry'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. 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. // // Metadata attached to errors returned by unary handlers is always sent as // HTTP headers, regardless of the protocol. Metadata attached to errors // returned by streaming handlers may be sent as HTTP headers, HTTP trailers, // or a block of in-body metadata, depending on the protocol in use and whether // or not the handler has already written messages to the stream. // // 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.pb) } 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 } // 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 } } func asMaxBytesError(err error, tmpl string, args ...any) *Error { var maxBytesErr *http.MaxBytesError if ok := errors.As(err, &maxBytesErr); !ok { return nil } prefix := fmt.Sprintf(tmpl, args...) return errorf(CodeResourceExhausted, "%s: exceeded %d byte http.MaxBytesReader limit", prefix, maxBytesErr.Limit) } func typeNameFromURL(url string) string { return url[strings.LastIndexByte(url, '/')+1:] } connect-go-1.13.0/error_example_test.go000066400000000000000000000041471453471351600201010ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "context" "errors" "fmt" "net/http" connect "connectrpc.com/connect" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" ) func ExampleError_Message() { err := fmt.Errorf( "another: %w", connect.NewError(connect.CodeUnavailable, errors.New("failed to foo")), ) if connectErr := (&connect.Error{}); errors.As(err, &connectErr) { fmt.Println("underlying error message:", connectErr.Message()) } // Output: // underlying error message: failed to foo } func ExampleIsNotModifiedError() { // Assume that the server from NewNotModifiedError's example is running on // localhost:8080. client := pingv1connect.NewPingServiceClient( http.DefaultClient, "http://localhost:8080", // Enable client-side support for HTTP GETs. connect.WithHTTPGet(), ) req := connect.NewRequest(&pingv1.PingRequest{Number: 42}) first, err := client.Ping(context.Background(), req) if err != nil { fmt.Println(err) return } // If the server set an Etag, we can use it to cache the response. etag := first.Header().Get("Etag") if etag == "" { fmt.Println("no Etag in response headers") return } fmt.Println("cached response with Etag", etag) // Now we'd like to make the same request again, but avoid re-fetching the // response if possible. req.Header().Set("If-None-Match", etag) _, err = client.Ping(context.Background(), req) if connect.IsNotModifiedError(err) { fmt.Println("can reuse cached response") } } connect-go-1.13.0/error_not_modified_example_test.go000066400000000000000000000043241453471351600226160ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "context" "net/http" "strconv" connect "connectrpc.com/connect" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" ) // ExampleCachingServer is an example of how servers can take advantage the // Connect protocol's support for HTTP-level caching. The Protobuf // definition for this API is in proto/connect/ping/v1/ping.proto. type ExampleCachingPingServer struct { pingv1connect.UnimplementedPingServiceHandler } // Ping is idempotent and free of side effects (and the Protobuf schema // indicates this), so clients using the Connect protocol may call it with HTTP // GET requests. This implementation uses Etags to manage client-side caching. func (*ExampleCachingPingServer) Ping( _ context.Context, req *connect.Request[pingv1.PingRequest], ) (*connect.Response[pingv1.PingResponse], error) { resp := connect.NewResponse(&pingv1.PingResponse{ Number: req.Msg.GetNumber(), }) // Our hashing logic is simple: we use the number in the PingResponse. hash := strconv.FormatInt(resp.Msg.GetNumber(), 10) // If the request was an HTTP GET, we'll need to check if the client already // has the response cached. if req.HTTPMethod() == http.MethodGet && req.Header().Get("If-None-Match") == hash { return nil, connect.NewNotModifiedError(http.Header{ "Etag": []string{hash}, }) } resp.Header().Set("Etag", hash) return resp, nil } func ExampleNewNotModifiedError() { mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(&ExampleCachingPingServer{})) _ = http.ListenAndServe("localhost:8080", mux) } connect-go-1.13.0/error_test.go000066400000000000000000000103451453471351600163630ustar00rootroot00000000000000// Copyright 2021-2023 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" "fmt" "strings" "testing" "time" "connectrpc.com/connect/internal/assert" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" ) func TestErrorNilUnderlying(t *testing.T) { t.Parallel() err := NewError(CodeUnknown, nil) assert.NotNil(t, err) assert.Equal(t, err.Error(), CodeUnknown.String()) assert.Equal(t, err.Code(), CodeUnknown) assert.Zero(t, err.Details()) detail, detailErr := NewErrorDetail(&emptypb.Empty{}) assert.Nil(t, detailErr) err.AddDetail(detail) assert.Equal(t, len(err.Details()), 1) assert.Equal(t, err.Details()[0].Type(), "google.protobuf.Empty") err.Meta().Set("foo", "bar") assert.Equal(t, err.Meta().Get("foo"), "bar") assert.Equal(t, CodeOf(err), CodeUnknown) } func TestErrorFormatting(t *testing.T) { t.Parallel() assert.Equal( t, NewError(CodeUnavailable, errors.New("")).Error(), CodeUnavailable.String(), ) got := NewError(CodeUnavailable, errors.New("foo")).Error() assert.True(t, strings.Contains(got, CodeUnavailable.String())) assert.True(t, strings.Contains(got, "foo")) } func TestErrorCode(t *testing.T) { t.Parallel() err := fmt.Errorf( "another: %w", NewError(CodeUnavailable, errors.New("foo")), ) connectErr, ok := asError(err) assert.True(t, ok) assert.Equal(t, connectErr.Code(), CodeUnavailable) } func TestCodeOf(t *testing.T) { t.Parallel() assert.Equal( t, CodeOf(NewError(CodeUnavailable, errors.New("foo"))), CodeUnavailable, ) assert.Equal(t, CodeOf(errors.New("foo")), CodeUnknown) } func TestErrorDetails(t *testing.T) { t.Parallel() second := durationpb.New(time.Second) detail, err := NewErrorDetail(second) assert.Nil(t, err) connectErr := NewError(CodeUnknown, errors.New("error with details")) assert.Zero(t, connectErr.Details()) connectErr.AddDetail(detail) assert.Equal(t, len(connectErr.Details()), 1) unmarshaled, err := connectErr.Details()[0].Value() assert.Nil(t, err) assert.Equal(t, unmarshaled, proto.Message(second)) secondBin, err := proto.Marshal(second) assert.Nil(t, err) assert.Equal(t, detail.Bytes(), secondBin) } func TestErrorIs(t *testing.T) { t.Parallel() // errors.New and fmt.Errorf return *errors.errorString. errors.Is // considers two *errors.errorStrings equal iff they have the same address. err := errors.New("oh no") assert.False(t, errors.Is(err, errors.New("oh no"))) assert.True(t, errors.Is(err, err)) // Our errors should have the same semantics. Note that we'd need to extend // the ErrorDetail interface to support value equality. connectErr := NewError(CodeUnavailable, err) assert.False(t, errors.Is(connectErr, NewError(CodeUnavailable, err))) assert.True(t, errors.Is(connectErr, connectErr)) } func TestTypeNameFromURL(t *testing.T) { t.Parallel() testCases := []struct { name string url string typeName string }{ { name: "no-prefix", url: "foo.bar.Baz", typeName: "foo.bar.Baz", }, { name: "standard-prefix", url: defaultAnyResolverPrefix + "foo.bar.Baz", typeName: "foo.bar.Baz", }, { name: "different-hostname", url: "abc.com/foo.bar.Baz", typeName: "foo.bar.Baz", }, { name: "additional-path-elements", url: defaultAnyResolverPrefix + "abc/def/foo.bar.Baz", typeName: "foo.bar.Baz", }, { name: "full-url", url: "https://abc.com/abc/def/foo.bar.Baz", typeName: "foo.bar.Baz", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() assert.Equal(t, typeNameFromURL(testCase.url), testCase.typeName) }) } } connect-go-1.13.0/error_writer.go000066400000000000000000000150731453471351600167230ustar00rootroot00000000000000// Copyright 2021-2023 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" ) // 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 allContentTypes map[string]struct{} grpcContentTypes map[string]struct{} grpcWebContentTypes map[string]struct{} unaryConnectContentTypes map[string]struct{} streamingConnectContentTypes map[string]struct{} } // NewErrorWriter constructs an ErrorWriter. To properly recognize supported // RPC Content-Types in net/http middleware, you must pass the same // HandlerOptions to NewErrorWriter and any wrapped Connect handlers. // Options supplied via [WithConditionalHandlerOptions] are ignored. func NewErrorWriter(opts ...HandlerOption) *ErrorWriter { config := newHandlerConfig("", StreamTypeUnary, opts) writer := &ErrorWriter{ bufferPool: config.BufferPool, protobuf: newReadOnlyCodecs(config.Codecs).Protobuf(), allContentTypes: make(map[string]struct{}), grpcContentTypes: make(map[string]struct{}), grpcWebContentTypes: make(map[string]struct{}), unaryConnectContentTypes: make(map[string]struct{}), streamingConnectContentTypes: make(map[string]struct{}), } for name := range config.Codecs { unary := connectContentTypeFromCodecName(StreamTypeUnary, name) writer.allContentTypes[unary] = struct{}{} writer.unaryConnectContentTypes[unary] = struct{}{} streaming := connectContentTypeFromCodecName(StreamTypeBidi, name) writer.streamingConnectContentTypes[streaming] = struct{}{} writer.allContentTypes[streaming] = struct{}{} } if config.HandleGRPC { writer.grpcContentTypes[grpcContentTypeDefault] = struct{}{} writer.allContentTypes[grpcContentTypeDefault] = struct{}{} for name := range config.Codecs { ct := grpcContentTypeFromCodecName(false /* web */, name) writer.grpcContentTypes[ct] = struct{}{} writer.allContentTypes[ct] = struct{}{} } } if config.HandleGRPCWeb { writer.grpcWebContentTypes[grpcWebContentTypeDefault] = struct{}{} writer.allContentTypes[grpcWebContentTypeDefault] = struct{}{} for name := range config.Codecs { ct := grpcContentTypeFromCodecName(true /* web */, name) writer.grpcWebContentTypes[ct] = struct{}{} writer.allContentTypes[ct] = struct{}{} } } return writer } // IsSupported checks whether a request is using one of the ErrorWriter's // supported RPC protocols. func (w *ErrorWriter) IsSupported(request *http.Request) bool { ctype := canonicalizeContentType(getHeaderCanonical(request.Header, headerContentType)) _, ok := w.allContentTypes[ctype] return ok } // 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. // // 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)) if _, ok := w.unaryConnectContentTypes[ctype]; ok { // Unary errors are always JSON. setHeaderCanonical(response.Header(), headerContentType, connectUnaryContentTypeJSON) return w.writeConnectUnary(response, err) } if _, ok := w.streamingConnectContentTypes[ctype]; ok { setHeaderCanonical(response.Header(), headerContentType, ctype) return w.writeConnectStreaming(response, err) } if _, ok := w.grpcContentTypes[ctype]; ok { setHeaderCanonical(response.Header(), headerContentType, ctype) return w.writeGRPC(response, err) } if _, ok := w.grpcWebContentTypes[ctype]; ok { setHeaderCanonical(response.Header(), headerContentType, ctype) return w.writeGRPCWeb(response, err) } return fmt.Errorf("unsupported Content-Type %q", ctype) } func (w *ErrorWriter) writeConnectUnary(response http.ResponseWriter, err error) error { if connectErr, ok := asError(err); ok { mergeHeaders(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 } connect-go-1.13.0/error_writer_example_test.go000066400000000000000000000043751453471351600215000ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "errors" "io" "log" "net/http" connect "connectrpc.com/connect" ) // NewHelloHandler is an example HTTP handler. In a real application, it might // handle RPCs, requests for HTML, or anything else. func NewHelloHandler() http.Handler { return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { io.WriteString(response, "Hello, world!") }) } // NewAuthenticatedHandler is an example of middleware that works with both RPC // and non-RPC clients. func NewAuthenticatedHandler(handler http.Handler) http.Handler { errorWriter := connect.NewErrorWriter() return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { // Dummy authentication logic. if request.Header.Get("Token") == "super-secret" { handler.ServeHTTP(response, request) return } defer request.Body.Close() defer io.Copy(io.Discard, request.Body) if errorWriter.IsSupported(request) { // Send a protocol-appropriate error to RPC clients, so that they receive // the right code, message, and any metadata or error details. unauthenticated := connect.NewError(connect.CodeUnauthenticated, errors.New("invalid token")) errorWriter.Write(response, request, unauthenticated) } else { // Send an error to non-RPC clients. response.WriteHeader(http.StatusUnauthorized) io.WriteString(response, "invalid token") } }) } func ExampleErrorWriter() { mux := http.NewServeMux() mux.Handle("/", NewHelloHandler()) srv := &http.Server{ Addr: ":8080", Handler: NewAuthenticatedHandler(mux), } if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalln(err) } } connect-go-1.13.0/example_init_test.go000066400000000000000000000023751453471351600177140ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "net/http" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" "connectrpc.com/connect/internal/memhttp" ) var examplePingServer *memhttp.Server func init() { // Generally, init functions are bad. However, we need to set up the server // before the examples run. // // To write testable examples that users can grok *and* can execute in the // playground we use an in memory pipe as network based playgrounds can // deadlock, see: // (https://github.com/golang/go/issues/48394) mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) examplePingServer = memhttp.NewServer(mux) } connect-go-1.13.0/go.mod000066400000000000000000000004531453471351600147510ustar00rootroot00000000000000module connectrpc.com/connect go 1.19 retract ( v1.10.0 // module cache poisoned, use v1.10.1 v1.9.0 // module cache poisoned, use v1.9.1 ) require ( github.com/google/go-cmp v0.5.9 golang.org/x/net v0.17.0 google.golang.org/protobuf v1.31.0 ) require golang.org/x/text v0.13.0 // indirect connect-go-1.13.0/go.sum000066400000000000000000000020061453471351600147720ustar00rootroot00000000000000github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= connect-go-1.13.0/handler.go000066400000000000000000000267531453471351600156220ustar00rootroot00000000000000// Copyright 2021-2023 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" "fmt" "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(fmt.Sprintf("%s returned nil *connect.Response and nil error", procedure)) //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 { var msg Req if err := config.Initializer.maybe(conn.Spec(), &msg); err != nil { return err } if err := conn.Receive(&msg); err != nil { return err } method := http.MethodPost if hasRequestMethod, ok := conn.(interface{ getHTTPMethod() string }); ok { method = hasRequestMethod.getHTTPMethod() } request := &Request[Req]{ Msg: &msg, spec: conn.Spec(), peer: conn.Peer(), header: conn.RequestHeader(), method: method, } response, err := untyped(ctx, request) if err != nil { return err } mergeHeaders(conn.ResponseHeader(), response.Header()) mergeHeaders(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), } } // 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, } 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(fmt.Sprintf("%s returned nil *connect.Response and nil error", procedure)) //nolint: forbidigo } mergeHeaders(conn.ResponseHeader(), res.header) mergeHeaders(conn.ResponseTrailer(), res.trailer) return conn.Send(res.Msg) }, ) } // 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 { var msg Req if err := config.Initializer.maybe(conn.Spec(), &msg); err != nil { return err } if err := conn.Receive(&msg); err != nil { return err } return implementation( ctx, &Request[Req]{ Msg: &msg, spec: conn.Spec(), peer: conn.Peer(), header: conn.RequestHeader(), method: http.MethodPost, }, &ServerStream[Res]{conn: conn}, ) }, ) } // 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 { 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 HandleGRPC bool HandleGRPCWeb bool 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), HandleGRPC: true, HandleGRPCWeb: true, 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{}} if c.HandleGRPC { protocols = append(protocols, &protocolGRPC{web: false}) } if c.HandleGRPCWeb { protocols = append(protocols, &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), } } connect-go-1.13.0/handler_example_test.go000066400000000000000000000070441453471351600203640ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "context" "errors" "io" "net/http" connect "connectrpc.com/connect" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" ) // ExamplePingServer implements some trivial business logic. The Protobuf // definition for this API is in proto/connect/ping/v1/ping.proto. type ExamplePingServer struct { pingv1connect.UnimplementedPingServiceHandler } // Ping implements pingv1connect.PingServiceHandler. func (*ExamplePingServer) Ping( _ context.Context, request *connect.Request[pingv1.PingRequest], ) (*connect.Response[pingv1.PingResponse], error) { return connect.NewResponse( &pingv1.PingResponse{ Number: request.Msg.GetNumber(), Text: request.Msg.GetText(), }, ), nil } // Sum implements pingv1connect.PingServiceHandler. func (p *ExamplePingServer) Sum(ctx context.Context, stream *connect.ClientStream[pingv1.SumRequest]) (*connect.Response[pingv1.SumResponse], error) { var sum int64 for stream.Receive() { sum += stream.Msg().GetNumber() } if stream.Err() != nil { return nil, stream.Err() } return connect.NewResponse(&pingv1.SumResponse{Sum: sum}), nil } // CountUp implements pingv1connect.PingServiceHandler. func (p *ExamplePingServer) CountUp(ctx context.Context, request *connect.Request[pingv1.CountUpRequest], stream *connect.ServerStream[pingv1.CountUpResponse]) error { for number := int64(1); number <= request.Msg.GetNumber(); number++ { if err := stream.Send(&pingv1.CountUpResponse{Number: number}); err != nil { return err } } return nil } // CumSum implements pingv1connect.PingServiceHandler. func (p *ExamplePingServer) CumSum(ctx context.Context, stream *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse]) error { var sum int64 for { msg, err := stream.Receive() if errors.Is(err, io.EOF) { return nil } else if err != nil { return err } sum += msg.GetNumber() if err := stream.Send(&pingv1.CumSumResponse{Sum: sum}); err != nil { return err } } } func Example_handler() { // protoc-gen-connect-go generates constructors that return plain net/http // Handlers, so they're compatible with most Go HTTP routers and middleware // (for example, net/http's StripPrefix). Each handler automatically supports // the Connect, gRPC, and gRPC-Web protocols. mux := http.NewServeMux() mux.Handle( pingv1connect.NewPingServiceHandler( &ExamplePingServer{}, // our business logic ), ) // You can serve gRPC's health and server reflection APIs using // connectrpc.com/grpchealth and connectrpc.com/grpcreflect. _ = http.ListenAndServeTLS( "localhost:8080", "internal/testdata/server.crt", "internal/testdata/server.key", mux, ) // To serve HTTP/2 requests without TLS (as many gRPC clients expect), import // golang.org/x/net/http2/h2c and golang.org/x/net/http2 and change to: // _ = http.ListenAndServe( // "localhost:8080", // h2c.NewHandler(mux, &http2.Server{}), // ) } connect-go-1.13.0/handler_ext_test.go000066400000000000000000000403661453471351600175350ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "bytes" "context" "encoding/binary" "encoding/json" "errors" "fmt" "io" "net/http" "strings" "sync" "testing" connect "connectrpc.com/connect" "connectrpc.com/connect/internal/assert" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" "connectrpc.com/connect/internal/memhttp/memhttptest" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/dynamicpb" ) func TestHandler_ServeHTTP(t *testing.T) { t.Parallel() path, handler := pingv1connect.NewPingServiceHandler(successPingServer{}) prefixed := http.NewServeMux() prefixed.Handle(path, handler) mux := http.NewServeMux() mux.Handle(path, handler) mux.Handle("/prefixed/", http.StripPrefix("/prefixed", prefixed)) const pingProcedure = pingv1connect.PingServicePingProcedure const sumProcedure = pingv1connect.PingServiceSumProcedure server := memhttptest.NewServer(t, mux) client := server.Client() t.Run("get_method_no_encoding", func(t *testing.T) { t.Parallel() request, err := http.NewRequestWithContext( context.Background(), http.MethodGet, server.URL()+pingProcedure, strings.NewReader(""), ) assert.Nil(t, err) resp, err := client.Do(request) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusUnsupportedMediaType) }) t.Run("get_method_bad_encoding", func(t *testing.T) { t.Parallel() request, err := http.NewRequestWithContext( context.Background(), http.MethodGet, server.URL()+pingProcedure+`?encoding=unk&message={}`, strings.NewReader(""), ) assert.Nil(t, err) resp, err := client.Do(request) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusUnsupportedMediaType) }) t.Run("get_method_body_not_allowed", func(t *testing.T) { t.Parallel() const queryStringSuffix = `?encoding=json&message={}` request, err := http.NewRequestWithContext( context.Background(), http.MethodGet, server.URL()+pingProcedure+queryStringSuffix, strings.NewReader("!"), // non-empty body ) assert.Nil(t, err) resp, err := client.Do(request) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusUnsupportedMediaType) // Same thing, but this time w/ a content-length header request, err = http.NewRequestWithContext( context.Background(), http.MethodGet, server.URL()+pingProcedure+queryStringSuffix, strings.NewReader("!"), // non-empty body ) assert.Nil(t, err) request.Header.Set("content-length", "1") resp, err = client.Do(request) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusUnsupportedMediaType) }) t.Run("idempotent_get_method", func(t *testing.T) { t.Parallel() request, err := http.NewRequestWithContext( context.Background(), http.MethodGet, server.URL()+pingProcedure+`?encoding=json&message={}`, strings.NewReader(""), ) assert.Nil(t, err) resp, err := client.Do(request) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusOK) }) t.Run("prefixed_get_method", func(t *testing.T) { t.Parallel() request, err := http.NewRequestWithContext( context.Background(), http.MethodGet, server.URL()+"/prefixed"+pingProcedure+`?encoding=json&message={}`, strings.NewReader(""), ) assert.Nil(t, err) resp, err := client.Do(request) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusOK) }) t.Run("method_not_allowed", func(t *testing.T) { t.Parallel() request, err := http.NewRequestWithContext( context.Background(), http.MethodGet, server.URL()+sumProcedure, strings.NewReader(""), ) assert.Nil(t, err) resp, err := client.Do(request) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusMethodNotAllowed) assert.Equal(t, resp.Header.Get("Allow"), http.MethodPost) }) t.Run("unsupported_content_type", func(t *testing.T) { t.Parallel() request, err := http.NewRequestWithContext( context.Background(), http.MethodPost, server.URL()+pingProcedure, strings.NewReader("{}"), ) assert.Nil(t, err) request.Header.Set("Content-Type", "application/x-custom-json") resp, err := client.Do(request) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusUnsupportedMediaType) assert.Equal(t, resp.Header.Get("Accept-Post"), strings.Join([]string{ "application/grpc", "application/grpc+json", "application/grpc+json; charset=utf-8", "application/grpc+proto", "application/grpc-web", "application/grpc-web+json", "application/grpc-web+json; charset=utf-8", "application/grpc-web+proto", "application/json", "application/json; charset=utf-8", "application/proto", }, ", ")) }) t.Run("charset_in_content_type_header", func(t *testing.T) { t.Parallel() req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, server.URL()+pingProcedure, strings.NewReader("{}"), ) assert.Nil(t, err) req.Header.Set("Content-Type", "application/json;Charset=Utf-8") resp, err := client.Do(req) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusOK) }) t.Run("unsupported_charset", func(t *testing.T) { t.Parallel() req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, server.URL()+pingProcedure, strings.NewReader("{}"), ) assert.Nil(t, err) req.Header.Set("Content-Type", "application/json; charset=shift-jis") resp, err := client.Do(req) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusUnsupportedMediaType) }) t.Run("unsupported_content_encoding", func(t *testing.T) { t.Parallel() req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, server.URL()+pingProcedure, strings.NewReader("{}"), ) assert.Nil(t, err) req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Encoding", "invalid") resp, err := client.Do(req) assert.Nil(t, err) defer resp.Body.Close() assert.Equal(t, resp.StatusCode, http.StatusNotFound) type errorMessage struct { Code string `json:"code,omitempty"` Message string `json:"message,omitempty"` } var message errorMessage err = json.NewDecoder(resp.Body).Decode(&message) assert.Nil(t, err) assert.Equal(t, message.Message, `unknown compression "invalid": supported encodings are gzip`) assert.Equal(t, message.Code, connect.CodeUnimplemented.String()) }) } func TestHandlerMaliciousPrefix(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(successPingServer{})) server := memhttptest.NewServer(t, mux) const ( concurrency = 256 spuriousSize = 1024 * 1024 * 512 // 512 MB ) var wg sync.WaitGroup start := make(chan struct{}) for i := 0; i < concurrency; i++ { body := make([]byte, 16) // Envelope prefix indicates a large payload which we're not actually // sending. binary.BigEndian.PutUint32(body[1:5], spuriousSize) req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, server.URL()+pingv1connect.PingServicePingProcedure, bytes.NewReader(body), ) assert.Nil(t, err) req.Header.Set("Content-Type", "application/grpc") wg.Add(1) go func(req *http.Request) { defer wg.Done() <-start response, err := server.Client().Do(req) if err == nil { _, _ = io.Copy(io.Discard, response.Body) response.Body.Close() } }(req) } close(start) wg.Wait() } func TestDynamicHandler(t *testing.T) { t.Parallel() initializer := func(spec connect.Spec, msg any) error { dynamic, ok := msg.(*dynamicpb.Message) if !ok { return nil } desc, ok := spec.Schema.(protoreflect.MethodDescriptor) if !ok { return fmt.Errorf("invalid schema type %T for %T message", spec.Schema, dynamic) } if spec.IsClient { *dynamic = *dynamicpb.NewMessage(desc.Output()) } else { *dynamic = *dynamicpb.NewMessage(desc.Input()) } return nil } t.Run("unary", func(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.Ping") assert.Nil(t, err) methodDesc, ok := desc.(protoreflect.MethodDescriptor) assert.True(t, ok) dynamicPing := func(_ context.Context, req *connect.Request[dynamicpb.Message]) (*connect.Response[dynamicpb.Message], error) { got := req.Msg.Get(methodDesc.Input().Fields().ByName("number")).Int() msg := dynamicpb.NewMessage(methodDesc.Output()) msg.Set( methodDesc.Output().Fields().ByName("number"), protoreflect.ValueOfInt64(got), ) return connect.NewResponse(msg), nil } mux := http.NewServeMux() mux.Handle("/connect.ping.v1.PingService/Ping", connect.NewUnaryHandler( "/connect.ping.v1.PingService/Ping", dynamicPing, connect.WithSchema(methodDesc), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithRequestInitializer(initializer), ), ) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) rsp, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{ Number: 42, })) if !assert.Nil(t, err) { return } got := rsp.Msg.Number assert.Equal(t, got, 42) }) t.Run("clientStream", func(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.Sum") assert.Nil(t, err) methodDesc, ok := desc.(protoreflect.MethodDescriptor) assert.True(t, ok) dynamicSum := func(_ context.Context, stream *connect.ClientStream[dynamicpb.Message]) (*connect.Response[dynamicpb.Message], error) { var sum int64 for stream.Receive() { got := stream.Msg().Get( methodDesc.Input().Fields().ByName("number"), ).Int() sum += got } msg := dynamicpb.NewMessage(methodDesc.Output()) msg.Set( methodDesc.Output().Fields().ByName("sum"), protoreflect.ValueOfInt64(sum), ) return connect.NewResponse(msg), nil } mux := http.NewServeMux() mux.Handle("/connect.ping.v1.PingService/Sum", connect.NewClientStreamHandler( "/connect.ping.v1.PingService/Sum", dynamicSum, connect.WithSchema(methodDesc), connect.WithRequestInitializer(initializer), ), ) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) stream := client.Sum(context.Background()) assert.Nil(t, stream.Send(&pingv1.SumRequest{Number: 42})) assert.Nil(t, stream.Send(&pingv1.SumRequest{Number: 42})) rsp, err := stream.CloseAndReceive() if !assert.Nil(t, err) { return } assert.Equal(t, rsp.Msg.Sum, 42*2) }) t.Run("serverStream", func(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.CountUp") assert.Nil(t, err) methodDesc, ok := desc.(protoreflect.MethodDescriptor) assert.True(t, ok) dynamicCountUp := func(_ context.Context, req *connect.Request[dynamicpb.Message], stream *connect.ServerStream[dynamicpb.Message]) error { number := req.Msg.Get(methodDesc.Input().Fields().ByName("number")).Int() for i := int64(1); i <= number; i++ { msg := dynamicpb.NewMessage(methodDesc.Output()) msg.Set( methodDesc.Output().Fields().ByName("number"), protoreflect.ValueOfInt64(i), ) if err := stream.Send(msg); err != nil { return err } } return nil } mux := http.NewServeMux() mux.Handle("/connect.ping.v1.PingService/CountUp", connect.NewServerStreamHandler( "/connect.ping.v1.PingService/CountUp", dynamicCountUp, connect.WithSchema(methodDesc), connect.WithRequestInitializer(initializer), ), ) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) stream, err := client.CountUp(context.Background(), connect.NewRequest(&pingv1.CountUpRequest{ Number: 2, })) if !assert.Nil(t, err) { return } var sum int64 for stream.Receive() { sum += stream.Msg().Number } assert.Nil(t, stream.Err()) assert.Equal(t, sum, 3) // 1 + 2 }) t.Run("bidi", func(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.CumSum") assert.Nil(t, err) methodDesc, ok := desc.(protoreflect.MethodDescriptor) assert.True(t, ok) dynamicCumSum := func( _ context.Context, stream *connect.BidiStream[dynamicpb.Message, dynamicpb.Message], ) error { var sum int64 for { msg, err := stream.Receive() if errors.Is(err, io.EOF) { return nil } else if err != nil { return err } got := msg.Get(methodDesc.Input().Fields().ByName("number")).Int() sum += got out := dynamicpb.NewMessage(methodDesc.Output()) out.Set( methodDesc.Output().Fields().ByName("sum"), protoreflect.ValueOfInt64(sum), ) if err := stream.Send(out); err != nil { return err } } } mux := http.NewServeMux() mux.Handle("/connect.ping.v1.PingService/CumSum", connect.NewBidiStreamHandler( "/connect.ping.v1.PingService/CumSum", dynamicCumSum, connect.WithSchema(methodDesc), connect.WithRequestInitializer(initializer), ), ) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) stream := client.CumSum(context.Background()) assert.Nil(t, stream.Send(&pingv1.CumSumRequest{Number: 1})) msg, err := stream.Receive() if !assert.Nil(t, err) { return } assert.Equal(t, msg.Sum, int64(1)) assert.Nil(t, stream.CloseRequest()) assert.Nil(t, stream.CloseResponse()) }) t.Run("option", func(t *testing.T) { t.Parallel() desc, err := protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.Ping") assert.Nil(t, err) methodDesc, ok := desc.(protoreflect.MethodDescriptor) assert.True(t, ok) dynamicPing := func(_ context.Context, req *connect.Request[dynamicpb.Message]) (*connect.Response[dynamicpb.Message], error) { got := req.Msg.Get(methodDesc.Input().Fields().ByName("number")).Int() msg := dynamicpb.NewMessage(methodDesc.Output()) msg.Set( methodDesc.Output().Fields().ByName("number"), protoreflect.ValueOfInt64(got), ) return connect.NewResponse(msg), nil } optionCalled := false mux := http.NewServeMux() mux.Handle("/connect.ping.v1.PingService/Ping", connect.NewUnaryHandler( "/connect.ping.v1.PingService/Ping", dynamicPing, connect.WithSchema(methodDesc), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithRequestInitializer( func(spec connect.Spec, msg any) error { assert.NotNil(t, spec) assert.NotNil(t, msg) dynamic, ok := msg.(*dynamicpb.Message) if !assert.True(t, ok) { return fmt.Errorf("unexpected message type: %T", msg) } *dynamic = *dynamicpb.NewMessage(methodDesc.Input()) optionCalled = true return nil }, ), ), ) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient(server.Client(), server.URL()) rsp, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{ Number: 42, })) if !assert.Nil(t, err) { return } got := rsp.Msg.Number assert.Equal(t, got, 42) assert.True(t, optionCalled) }) } type successPingServer struct { pingv1connect.UnimplementedPingServiceHandler } func (successPingServer) Ping(context.Context, *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { return &connect.Response[pingv1.PingResponse]{}, nil } connect-go-1.13.0/handler_stream.go000066400000000000000000000136641453471351600171720ustar00rootroot00000000000000// Copyright 2021-2023 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 } connect-go-1.13.0/handler_stream_test.go000066400000000000000000000027651453471351600202310ustar00rootroot00000000000000// Copyright 2021-2023 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" "testing" "connectrpc.com/connect/internal/assert" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" ) func TestClientStreamIterator(t *testing.T) { t.Parallel() // The server's view of a client streaming RPC is an iterator. For safety, // and to match grpc-go's behavior, we should allocate a new message for each // iteration. stream := &ClientStream[pingv1.PingRequest]{ conn: &nopStreamingHandlerConn{}, } assert.True(t, stream.Receive()) first := fmt.Sprintf("%p", stream.Msg()) assert.True(t, stream.Receive()) second := fmt.Sprintf("%p", stream.Msg()) assert.NotEqual(t, first, second, assert.Sprintf("should allocate a new message for each iteration")) } type nopStreamingHandlerConn struct { StreamingHandlerConn } func (nopStreamingHandlerConn) Receive(msg any) error { return nil } func (nopStreamingHandlerConn) Spec() Spec { return Spec{} } connect-go-1.13.0/header.go000066400000000000000000000055571453471351600154340ustar00rootroot00000000000000// Copyright 2021-2023 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" ) // 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 k, vals := range from { into[k] = append(into[k], 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) } // addHeaderCanonical is a shortcut for Header.Add() which // bypasses the CanonicalMIMEHeaderKey operation when we // know the key is already in canonical form. func addHeaderCanonical(h http.Header, key, value string) { h[key] = append(h[key], value) } connect-go-1.13.0/header_test.go000066400000000000000000000027431453471351600164650ustar00rootroot00000000000000// Copyright 2021-2023 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" "net/http" "testing" "testing/quick" "connectrpc.com/connect/internal/assert" ) func TestBinaryEncodingQuick(t *testing.T) { t.Parallel() roundtrip := func(binary []byte) bool { encoded := EncodeBinaryHeader(binary) decoded, err := DecodeBinaryHeader(encoded) if err != nil { // We want to abort immediately. Don't use our assert package. t.Fatalf("decode error: %v", err) } return bytes.Equal(decoded, binary) } if err := quick.Check(roundtrip, nil /* config */); err != nil { t.Error(err) } } func TestHeaderMerge(t *testing.T) { t.Parallel() header := http.Header{ "Foo": []string{"one"}, } mergeHeaders(header, http.Header{ "Foo": []string{"two"}, "Bar": []string{"one"}, "Baz": nil, }) expect := http.Header{ "Foo": []string{"one", "two"}, "Bar": []string{"one"}, "Baz": nil, } assert.Equal(t, header, expect) } connect-go-1.13.0/idempotency_level.go000066400000000000000000000056001453471351600177000ustar00rootroot00000000000000// Copyright 2021-2023 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) } connect-go-1.13.0/interceptor.go000066400000000000000000000074071453471351600165360ustar00rootroot00000000000000// Copyright 2021-2023 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" ) // 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 replace the context, 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 = interceptor.WrapUnary(next) } return next } func (c *chain) WrapStreamingClient(next StreamingClientFunc) StreamingClientFunc { for _, interceptor := range c.interceptors { next = interceptor.WrapStreamingClient(next) } return next } func (c *chain) WrapStreamingHandler(next StreamingHandlerFunc) StreamingHandlerFunc { for _, interceptor := range c.interceptors { next = interceptor.WrapStreamingHandler(next) } return next } connect-go-1.13.0/interceptor_example_test.go000066400000000000000000000066501453471351600213070ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "context" "log" "os" connect "connectrpc.com/connect" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" ) func ExampleUnaryInterceptorFunc() { logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */) loggingInterceptor := connect.UnaryInterceptorFunc( func(next connect.UnaryFunc) connect.UnaryFunc { return connect.UnaryFunc(func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) { logger.Println("calling:", request.Spec().Procedure) logger.Println("request:", request.Any()) response, err := next(ctx, request) if err != nil { logger.Println("error:", err) } else { logger.Println("response:", response.Any()) } return response, err }) }, ) client := pingv1connect.NewPingServiceClient( examplePingServer.Client(), examplePingServer.URL(), connect.WithInterceptors(loggingInterceptor), ) if _, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{Number: 42})); err != nil { logger.Println("error:", err) return } // Output: // calling: /connect.ping.v1.PingService/Ping // request: number:42 // response: number:42 } func ExampleWithInterceptors() { logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */) outer := connect.UnaryInterceptorFunc( func(next connect.UnaryFunc) connect.UnaryFunc { return connect.UnaryFunc(func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { logger.Println("outer interceptor: before call") res, err := next(ctx, req) logger.Println("outer interceptor: after call") return res, err }) }, ) inner := connect.UnaryInterceptorFunc( func(next connect.UnaryFunc) connect.UnaryFunc { return connect.UnaryFunc(func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { logger.Println("inner interceptor: before call") res, err := next(ctx, req) logger.Println("inner interceptor: after call") return res, err }) }, ) client := pingv1connect.NewPingServiceClient( examplePingServer.Client(), examplePingServer.URL(), connect.WithInterceptors(outer, inner), ) if _, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{})); err != nil { logger.Println("error:", err) return } // Output: // outer interceptor: before call // inner interceptor: before call // inner interceptor: after call // outer interceptor: after call } func ExampleWithConditionalHandlerOptions() { connect.WithConditionalHandlerOptions(func(spec connect.Spec) []connect.HandlerOption { var options []connect.HandlerOption if spec.Procedure == pingv1connect.PingServicePingProcedure { options = append(options, connect.WithReadMaxBytes(1024)) } return options }) // Output: } connect-go-1.13.0/interceptor_ext_test.go000066400000000000000000000315441453471351600204540ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "context" "fmt" "net/http" "sync/atomic" "testing" connect "connectrpc.com/connect" "connectrpc.com/connect/internal/assert" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" "connectrpc.com/connect/internal/memhttp/memhttptest" ) func TestOnionOrderingEndToEnd(t *testing.T) { t.Parallel() // Helper function: returns a function that asserts that there's some value // set for header "expect", and adds a value for header "add". newInspector := func(expect, add string) func(connect.Spec, http.Header) { return func(spec connect.Spec, header http.Header) { if expect != "" { assert.NotZero( t, header.Get(expect), assert.Sprintf( "%s (IsClient %v): header %q missing: %v", spec.Procedure, spec.IsClient, expect, header, ), ) } header.Set(add, "v") } } // Helper function: asserts that there's a value present for header keys // "one", "two", "three", and "four". assertAllPresent := func(spec connect.Spec, header http.Header) { for _, key := range []string{"one", "two", "three", "four"} { assert.NotZero( t, header.Get(key), assert.Sprintf( "%s (IsClient %v): checking all headers, %q missing: %v", spec.Procedure, spec.IsClient, key, header, ), ) } } var client1, client2, client3, handler1, handler2, handler3 atomic.Int32 // The client and handler interceptor onions are the meat of the test. The // order of interceptor execution must be the same for unary and streaming // procedures. // // Requests should fall through the client onion from top to bottom, traverse // the network, and then fall through the handler onion from top to bottom. // Responses should climb up the handler onion, traverse the network, and // then climb up the client onion. // // The request and response sides of this onion are numbered to make the // intended order clear. clientOnion := connect.WithInterceptors( newHeaderInterceptor( &client1, // 1 (start). request: should see protocol-related headers func(_ connect.Spec, h http.Header) { assert.NotZero(t, h.Get("Content-Type")) }, // 12 (end). response: check "one"-"four" assertAllPresent, ), newHeaderInterceptor( &client2, newInspector("", "one"), // 2. request: add header "one" newInspector("three", "four"), // 11. response: check "three", add "four" ), newHeaderInterceptor( &client3, newInspector("one", "two"), // 3. request: check "one", add "two" newInspector("two", "three"), // 10. response: check "two", add "three" ), ) handlerOnion := connect.WithInterceptors( newHeaderInterceptor( &handler1, newInspector("two", "three"), // 4. request: check "two", add "three" newInspector("one", "two"), // 9. response: check "one", add "two" ), newHeaderInterceptor( &handler2, newInspector("three", "four"), // 5. request: check "three", add "four" newInspector("", "one"), // 8. response: add "one" ), newHeaderInterceptor( &handler3, assertAllPresent, // 6. request: check "one"-"four" nil, // 7. response: no-op ), ) mux := http.NewServeMux() mux.Handle( pingv1connect.NewPingServiceHandler( pingServer{}, handlerOnion, ), ) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), clientOnion, ) _, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{Number: 10})) assert.Nil(t, err) // make sure the interceptors were actually invoked assert.Equal(t, int32(1), client1.Load()) assert.Equal(t, int32(1), client2.Load()) assert.Equal(t, int32(1), client3.Load()) assert.Equal(t, int32(1), handler1.Load()) assert.Equal(t, int32(1), handler2.Load()) assert.Equal(t, int32(1), handler3.Load()) responses, err := client.CountUp(context.Background(), connect.NewRequest(&pingv1.CountUpRequest{Number: 10})) assert.Nil(t, err) var sum int64 for responses.Receive() { sum += responses.Msg().GetNumber() } assert.Equal(t, sum, 55) assert.Nil(t, responses.Close()) // make sure the interceptors were invoked again assert.Equal(t, int32(2), client1.Load()) assert.Equal(t, int32(2), client2.Load()) assert.Equal(t, int32(2), client3.Load()) assert.Equal(t, int32(2), handler1.Load()) assert.Equal(t, int32(2), handler2.Load()) assert.Equal(t, int32(2), handler3.Load()) } func TestEmptyUnaryInterceptorFunc(t *testing.T) { t.Parallel() mux := http.NewServeMux() interceptor := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) { return next(ctx, request) } }) mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{}, connect.WithInterceptors(interceptor))) server := memhttptest.NewServer(t, mux) connectClient := pingv1connect.NewPingServiceClient(server.Client(), server.URL(), connect.WithInterceptors(interceptor)) _, err := connectClient.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{})) assert.Nil(t, err) sumStream := connectClient.Sum(context.Background()) assert.Nil(t, sumStream.Send(&pingv1.SumRequest{Number: 1})) resp, err := sumStream.CloseAndReceive() assert.Nil(t, err) assert.NotNil(t, resp) countUpStream, err := connectClient.CountUp(context.Background(), connect.NewRequest(&pingv1.CountUpRequest{})) assert.Nil(t, err) for countUpStream.Receive() { assert.NotNil(t, countUpStream.Msg()) } assert.Nil(t, countUpStream.Close()) } func TestInterceptorFuncAccessingHTTPMethod(t *testing.T) { t.Parallel() clientChecker := &httpMethodChecker{client: true} handlerChecker := &httpMethodChecker{} mux := http.NewServeMux() mux.Handle( pingv1connect.NewPingServiceHandler( pingServer{}, connect.WithInterceptors(handlerChecker), ), ) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), connect.WithInterceptors(clientChecker), ) pingReq := connect.NewRequest(&pingv1.PingRequest{Number: 10}) assert.Equal(t, "", pingReq.HTTPMethod()) _, err := client.Ping(context.Background(), pingReq) assert.Nil(t, err) assert.Equal(t, http.MethodPost, pingReq.HTTPMethod()) // make sure interceptor was invoked assert.Equal(t, int32(1), clientChecker.count.Load()) assert.Equal(t, int32(1), handlerChecker.count.Load()) countUpReq := connect.NewRequest(&pingv1.CountUpRequest{Number: 10}) assert.Equal(t, "", countUpReq.HTTPMethod()) responses, err := client.CountUp(context.Background(), countUpReq) assert.Nil(t, err) var sum int64 for responses.Receive() { sum += responses.Msg().GetNumber() } assert.Equal(t, sum, 55) assert.Nil(t, responses.Close()) assert.Equal(t, http.MethodPost, countUpReq.HTTPMethod()) // make sure interceptor was invoked again assert.Equal(t, int32(2), clientChecker.count.Load()) assert.Equal(t, int32(2), handlerChecker.count.Load()) } // headerInterceptor makes it easier to write interceptors that inspect or // mutate HTTP headers. It applies the same logic to unary and streaming // procedures, wrapping the send or receive side of the stream as appropriate. // // It's useful as a testing harness to make sure that we're chaining // interceptors in the correct order. type headerInterceptor struct { counter *atomic.Int32 inspectRequestHeader func(connect.Spec, http.Header) inspectResponseHeader func(connect.Spec, http.Header) } // newHeaderInterceptor constructs a headerInterceptor. Nil function pointers // are treated as no-ops. func newHeaderInterceptor( counter *atomic.Int32, inspectRequestHeader func(connect.Spec, http.Header), inspectResponseHeader func(connect.Spec, http.Header), ) *headerInterceptor { interceptor := headerInterceptor{ counter: counter, inspectRequestHeader: inspectRequestHeader, inspectResponseHeader: inspectResponseHeader, } if interceptor.inspectRequestHeader == nil { interceptor.inspectRequestHeader = func(_ connect.Spec, _ http.Header) {} } if interceptor.inspectResponseHeader == nil { interceptor.inspectResponseHeader = func(_ connect.Spec, _ http.Header) {} } return &interceptor } func (h *headerInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { h.counter.Add(1) h.inspectRequestHeader(req.Spec(), req.Header()) res, err := next(ctx, req) if err != nil { return nil, err } h.inspectResponseHeader(req.Spec(), res.Header()) return res, nil } } func (h *headerInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { h.counter.Add(1) return &headerInspectingClientConn{ StreamingClientConn: next(ctx, spec), inspectRequestHeader: h.inspectRequestHeader, inspectResponseHeader: h.inspectResponseHeader, } } } func (h *headerInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return func(ctx context.Context, conn connect.StreamingHandlerConn) error { h.counter.Add(1) h.inspectRequestHeader(conn.Spec(), conn.RequestHeader()) return next(ctx, &headerInspectingHandlerConn{ StreamingHandlerConn: conn, inspectResponseHeader: h.inspectResponseHeader, }) } } type headerInspectingHandlerConn struct { connect.StreamingHandlerConn inspectedResponse bool inspectResponseHeader func(connect.Spec, http.Header) } func (hc *headerInspectingHandlerConn) Send(msg any) error { if !hc.inspectedResponse { hc.inspectResponseHeader(hc.Spec(), hc.ResponseHeader()) hc.inspectedResponse = true } return hc.StreamingHandlerConn.Send(msg) } type headerInspectingClientConn struct { connect.StreamingClientConn inspectedRequest bool inspectRequestHeader func(connect.Spec, http.Header) inspectedResponse bool inspectResponseHeader func(connect.Spec, http.Header) } func (cc *headerInspectingClientConn) Send(msg any) error { if !cc.inspectedRequest { cc.inspectRequestHeader(cc.Spec(), cc.RequestHeader()) cc.inspectedRequest = true } return cc.StreamingClientConn.Send(msg) } func (cc *headerInspectingClientConn) Receive(msg any) error { err := cc.StreamingClientConn.Receive(msg) if !cc.inspectedResponse { cc.inspectResponseHeader(cc.Spec(), cc.ResponseHeader()) cc.inspectedResponse = true } return err } type httpMethodChecker struct { client bool count atomic.Int32 } func (h *httpMethodChecker) WrapUnary(unaryFunc connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { h.count.Add(1) if h.client { // should be blank until after we make request if req.HTTPMethod() != "" { return nil, fmt.Errorf("expected blank HTTP method but instead got %q", req.HTTPMethod()) } } else { // server interceptors see method from the start // NB: In theory, the method could also be GET, not just POST. But for the // configuration under test, it will always be POST. if req.HTTPMethod() != http.MethodPost { return nil, fmt.Errorf("expected HTTP method %s but instead got %q", http.MethodPost, req.HTTPMethod()) } } resp, err := unaryFunc(ctx, req) // NB: In theory, the method could also be GET, not just POST. But for the // configuration under test, it will always be POST. if req.HTTPMethod() != http.MethodPost { return nil, fmt.Errorf("expected HTTP method %s but instead got %q", http.MethodPost, req.HTTPMethod()) } return resp, err } } func (h *httpMethodChecker) WrapStreamingClient(clientFunc connect.StreamingClientFunc) connect.StreamingClientFunc { return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { // method not exposed to streaming interceptor, but that's okay because it's always POST for streams h.count.Add(1) return clientFunc(ctx, spec) } } func (h *httpMethodChecker) WrapStreamingHandler(handlerFunc connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return func(ctx context.Context, conn connect.StreamingHandlerConn) error { // method not exposed to streaming interceptor, but that's okay because it's always POST for streams h.count.Add(1) return handlerFunc(ctx, conn) } } connect-go-1.13.0/internal/000077500000000000000000000000001453471351600154555ustar00rootroot00000000000000connect-go-1.13.0/internal/assert/000077500000000000000000000000001453471351600167565ustar00rootroot00000000000000connect-go-1.13.0/internal/assert/assert.go000066400000000000000000000131511453471351600206070ustar00rootroot00000000000000// Copyright 2021-2023 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 assert is a minimal assert package using generics. // // This prevents connect from needing additional dependencies. package assert import ( "bytes" "errors" "fmt" "reflect" "regexp" "testing" "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/testing/protocmp" ) // Equal asserts that two values are equal. func Equal[T any](tb testing.TB, got, want T, options ...Option) bool { tb.Helper() if cmpEqual(got, want) { return true } report(tb, got, want, "assert.Equal", true /* showWant */, options...) return false } // NotEqual asserts that two values aren't equal. func NotEqual[T any](tb testing.TB, got, want T, options ...Option) bool { tb.Helper() if !cmpEqual(got, want) { return true } report(tb, got, want, "assert.NotEqual", true /* showWant */, options...) return false } // Nil asserts that the value is nil. func Nil(tb testing.TB, got any, options ...Option) bool { tb.Helper() if isNil(got) { return true } report(tb, got, nil, "assert.Nil", false /* showWant */, options...) return false } // NotNil asserts that the value isn't nil. func NotNil(tb testing.TB, got any, options ...Option) bool { tb.Helper() if !isNil(got) { return true } report(tb, got, nil, "assert.NotNil", false /* showWant */, options...) return false } // Zero asserts that the value is its type's zero value. func Zero[T any](tb testing.TB, got T, options ...Option) bool { tb.Helper() var want T if cmpEqual(got, want) { return true } report(tb, got, want, fmt.Sprintf("assert.Zero (type %T)", got), false /* showWant */, options...) return false } // NotZero asserts that the value is non-zero. func NotZero[T any](tb testing.TB, got T, options ...Option) bool { tb.Helper() var want T if !cmpEqual(got, want) { return true } report(tb, got, want, fmt.Sprintf("assert.NotZero (type %T)", got), false /* showWant */, options...) return false } // Match asserts that the value matches a regexp. func Match(tb testing.TB, got, want string, options ...Option) bool { tb.Helper() re, err := regexp.Compile(want) if err != nil { tb.Fatalf("invalid regexp %q: %v", want, err) } if re.MatchString(got) { return true } report(tb, got, want, "assert.Match", true /* showWant */, options...) return false } // ErrorIs asserts that "want" is in "got's" error chain. See the standard // library's errors package for details on error chains. On failure, output is // identical to Equal. func ErrorIs(tb testing.TB, got, want error, options ...Option) bool { tb.Helper() if errors.Is(got, want) { return true } report(tb, got, want, "assert.ErrorIs", true /* showWant */, options...) return false } // False asserts that "got" is false. func False(tb testing.TB, got bool, options ...Option) bool { tb.Helper() if !got { return true } report(tb, got, false, "assert.False", false /* showWant */, options...) return false } // True asserts that "got" is true. func True(tb testing.TB, got bool, options ...Option) bool { tb.Helper() if got { return true } report(tb, got, true, "assert.True", false /* showWant */, options...) return false } // Panics asserts that the function called panics. func Panics(tb testing.TB, panicker func(), options ...Option) { tb.Helper() defer func() { if r := recover(); r == nil { report(tb, r, nil, "assert.Panic", false /* showWant */, options...) } }() panicker() } // An Option configures an assertion. type Option interface { // Only option we've needed so far is a formatted message, so we can keep // this simple. message() string } // Sprintf adds a user-defined message to the assertion's output. The arguments // are passed directly to fmt.Sprintf for formatting. // // If Sprintf is passed multiple times, only the last message is used. func Sprintf(template string, args ...any) Option { return &sprintfOption{fmt.Sprintf(template, args...)} } type sprintfOption struct { msg string } func (o *sprintfOption) message() string { return o.msg } func report(tb testing.TB, got, want any, desc string, showWant bool, options ...Option) { tb.Helper() buffer := &bytes.Buffer{} if len(options) > 0 { buffer.WriteString(options[len(options)-1].message()) } buffer.WriteString("\n") fmt.Fprintf(buffer, "assertion:\t%s\n", desc) fmt.Fprintf(buffer, "got:\t%+v\n", got) if showWant { fmt.Fprintf(buffer, "want:\t%+v\n", want) } tb.Error(buffer.String()) } func isNil(got any) bool { // Simple case, true only when the user directly passes a literal nil. if got == nil { return true } // Possibly more complex. Interfaces are a pair of words: a pointer to a type // and a pointer to a value. Because we're passing got as an interface, it's // likely that we've gotten a non-nil type and a nil value. This makes got // itself non-nil, but the user's code passed a nil value. val := reflect.ValueOf(got) switch val.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: return val.IsNil() default: return false } } func cmpEqual(got, want any) bool { return cmp.Equal(got, want, protocmp.Transform()) } connect-go-1.13.0/internal/assert/assert_test.go000066400000000000000000000040031453471351600216420ustar00rootroot00000000000000// Copyright 2021-2023 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 assert import ( "errors" "fmt" "testing" ) func TestAssertions(t *testing.T) { t.Parallel() t.Run("equal", func(t *testing.T) { t.Parallel() Equal(t, 1, 1, Sprintf("1 == %d", 1)) NotEqual(t, 1, 2) }) t.Run("nil", func(t *testing.T) { t.Parallel() Nil(t, nil) Nil(t, (*chan int)(nil)) Nil(t, (*func())(nil)) Nil(t, (*map[int]int)(nil)) Nil(t, (*pair)(nil)) Nil(t, (*[]int)(nil)) NotNil(t, make(chan int)) NotNil(t, func() {}) NotNil(t, any(1)) NotNil(t, make(map[int]int)) NotNil(t, &pair{}) NotNil(t, make([]int, 0)) NotNil(t, "foo") NotNil(t, 0) NotNil(t, false) NotNil(t, pair{}) }) t.Run("zero", func(t *testing.T) { t.Parallel() var n *int Zero(t, n) var p pair Zero(t, p) var null *pair Zero(t, null) var s []int Zero(t, s) var m map[string]string Zero(t, m) NotZero(t, 3) }) t.Run("error chain", func(t *testing.T) { t.Parallel() want := errors.New("base error") ErrorIs(t, fmt.Errorf("context: %w", want), want) }) t.Run("unexported fields", func(t *testing.T) { t.Parallel() // Two pairs differ only in an unexported field. p1 := pair{1, 2} p2 := pair{1, 3} NotEqual(t, p1, p2) }) t.Run("regexp", func(t *testing.T) { t.Parallel() Match(t, "foobar", `^foo`) }) t.Run("panics", func(t *testing.T) { t.Parallel() Panics(t, func() { panic("testing") }) //nolint:forbidigo }) } type pair struct { First, Second int } connect-go-1.13.0/internal/conformance/000077500000000000000000000000001453471351600177475ustar00rootroot00000000000000connect-go-1.13.0/internal/conformance/config.yaml000066400000000000000000000006431453471351600221030ustar00rootroot00000000000000features: versions: - HTTP_VERSION_1 - HTTP_VERSION_2 - HTTP_VERSION_3 protocols: - PROTOCOL_CONNECT - PROTOCOL_GRPC - PROTOCOL_GRPC_WEB codecs: - CODEC_PROTO - CODEC_JSON - CODEC_TEXT compressions: - COMPRESSION_IDENTITY - COMPRESSION_GZIP - COMPRESSION_BR - COMPRESSION_ZSTD - COMPRESSION_DEFLATE - COMPRESSION_SNAPPY supportsTlsClientCerts: true connect-go-1.13.0/internal/conformance/go.mod000066400000000000000000000037441453471351600210650ustar00rootroot00000000000000module connectrpc.com/connect/internal/conformance go 1.19 require connectrpc.com/conformance v1.0.0-rc1 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230914171853-63dfe56cc2c4.1 // indirect connectrpc.com/connect v1.11.1 // indirect github.com/andybalholm/brotli v1.0.6 // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect github.com/bufbuild/protovalidate-go v0.3.2 // indirect github.com/bufbuild/protoyaml-go v0.1.4 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/cel-go v0.18.1 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.17.2 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/quic-go/quic-go v0.40.0 // indirect github.com/rs/cors v1.10.1 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect go.uber.org/mock v0.3.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.13.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace connectrpc.com/connect => ../../ connect-go-1.13.0/internal/conformance/go.sum000066400000000000000000000236321453471351600211100ustar00rootroot00000000000000buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230914171853-63dfe56cc2c4.1 h1:2gmp+PRca1fqQHf/WMKOgu9inVb0R0N07TucgY3QZCQ= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230914171853-63dfe56cc2c4.1/go.mod h1:xafc+XIsTxTy76GJQ1TKgvJWsSugFBqMaN27WhUblew= connectrpc.com/conformance v1.0.0-rc1 h1:XkNv4EU8SaXIDUsua2V9UHw0IRn4tqfNeMakHNq9/BU= connectrpc.com/conformance v1.0.0-rc1/go.mod h1:hwMCPIzimL0c5WYnR23kSJ8ngRAIWqIZIG85pHSB03c= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/bufbuild/protovalidate-go v0.3.2 h1:7sG1R83PkCzOZb3P187gAchWFLHY6LQ8aVoUw6Wp9es= github.com/bufbuild/protovalidate-go v0.3.2/go.mod h1:ywZqKUjMhQA8fmhsc+0DUlMfan8/umJ+5mKvjdxAD3M= github.com/bufbuild/protoyaml-go v0.1.4 h1:wPSKIb/DkHwUK71Dqw5cUkLpohWD7JY+TeLBbrlN8nM= github.com/bufbuild/protoyaml-go v0.1.4/go.mod h1:6G7eGacFmps/ilH7uyfjv18HQ74Feri8I5dZi7XMs1o= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/cel-go v0.18.1 h1:V/lAXKq4C3BYLDy/ARzMtpkEEYfHQpZzVyzy69nEUjs= github.com/google/cel-go v0.18.1/go.mod h1:PVAybmSnWkNMUZR/tEWFUiJ1Np4Hz0MHsZJcgC4zln4= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw= github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 h1:U7+wNaVuSTaUqNvK2+osJ9ejEZxbjHHk8F2b6Hpx0AE= google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:RdyHbowztCGQySiCvQPgWQWgWhGnouTdCflKoDBt32U= google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= connect-go-1.13.0/internal/conformance/runconformance.sh000077500000000000000000000015661453471351600233350ustar00rootroot00000000000000#!/bin/bash set -euo pipefail cd "$(dirname "$0")" BINDIR="../../.tmp/bin" mkdir -p $BINDIR GO="${GO:-go}" # These will get built using current HEAD of this connect-go repo # thanks to replace directive in go.mod. So by testing the reference # implementations (which are written with connect-go), we are effectively # testing changes in this repo. $GO build -o $BINDIR/connectconformance connectrpc.com/conformance/cmd/connectconformance $GO build -o $BINDIR/referenceclient connectrpc.com/conformance/cmd/referenceclient $GO build -o $BINDIR/referenceserver connectrpc.com/conformance/cmd/referenceserver echo "Running conformance tests against client..." $BINDIR/connectconformance --mode client --conf config.yaml -- $BINDIR/referenceclient echo "Running conformance tests against server..." $BINDIR/connectconformance --mode server --conf config.yaml -- $BINDIR/referenceserver connect-go-1.13.0/internal/conformance/tools.go000066400000000000000000000012541453471351600214400ustar00rootroot00000000000000// Copyright 2021-2023 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 tools import ( _ "connectrpc.com/conformance/cmd/connectconformance" ) connect-go-1.13.0/internal/gen/000077500000000000000000000000001453471351600162265ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/000077500000000000000000000000001453471351600176575ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/collide/000077500000000000000000000000001453471351600212725ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/collide/v1/000077500000000000000000000000001453471351600216205ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/collide/v1/collide.pb.go000066400000000000000000000207241453471351600241670ustar00rootroot00000000000000// Copyright 2021-2023 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.31.0 // protoc (unknown) // source: connect/collide/v1/collide.proto package collidev1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) 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) ) type ImportRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *ImportRequest) Reset() { *x = ImportRequest{} if protoimpl.UnsafeEnabled { mi := &file_connect_collide_v1_collide_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ImportRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ImportRequest) ProtoMessage() {} func (x *ImportRequest) ProtoReflect() protoreflect.Message { mi := &file_connect_collide_v1_collide_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ImportRequest.ProtoReflect.Descriptor instead. func (*ImportRequest) Descriptor() ([]byte, []int) { return file_connect_collide_v1_collide_proto_rawDescGZIP(), []int{0} } type ImportResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *ImportResponse) Reset() { *x = ImportResponse{} if protoimpl.UnsafeEnabled { mi := &file_connect_collide_v1_collide_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ImportResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ImportResponse) ProtoMessage() {} func (x *ImportResponse) ProtoReflect() protoreflect.Message { mi := &file_connect_collide_v1_collide_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ImportResponse.ProtoReflect.Descriptor instead. func (*ImportResponse) Descriptor() ([]byte, []int) { return file_connect_collide_v1_collide_proto_rawDescGZIP(), []int{1} } var File_connect_collide_v1_collide_proto protoreflect.FileDescriptor var file_connect_collide_v1_collide_proto_rawDesc = []byte{ 0x0a, 0x20, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x0f, 0x0a, 0x0d, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x10, 0x0a, 0x0e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x63, 0x0a, 0x0e, 0x43, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x06, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x21, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xd2, 0x01, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x43, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x40, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x43, 0x58, 0xaa, 0x02, 0x12, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x12, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5c, 0x43, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5c, 0x43, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x14, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x3a, 0x3a, 0x43, 0x6f, 0x6c, 0x6c, 0x69, 0x64, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_connect_collide_v1_collide_proto_rawDescOnce sync.Once file_connect_collide_v1_collide_proto_rawDescData = file_connect_collide_v1_collide_proto_rawDesc ) func file_connect_collide_v1_collide_proto_rawDescGZIP() []byte { file_connect_collide_v1_collide_proto_rawDescOnce.Do(func() { file_connect_collide_v1_collide_proto_rawDescData = protoimpl.X.CompressGZIP(file_connect_collide_v1_collide_proto_rawDescData) }) return file_connect_collide_v1_collide_proto_rawDescData } var file_connect_collide_v1_collide_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_connect_collide_v1_collide_proto_goTypes = []interface{}{ (*ImportRequest)(nil), // 0: connect.collide.v1.ImportRequest (*ImportResponse)(nil), // 1: connect.collide.v1.ImportResponse } var file_connect_collide_v1_collide_proto_depIdxs = []int32{ 0, // 0: connect.collide.v1.CollideService.Import:input_type -> connect.collide.v1.ImportRequest 1, // 1: connect.collide.v1.CollideService.Import:output_type -> connect.collide.v1.ImportResponse 1, // [1:2] is the sub-list for method output_type 0, // [0:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_connect_collide_v1_collide_proto_init() } func file_connect_collide_v1_collide_proto_init() { if File_connect_collide_v1_collide_proto != nil { return } if !protoimpl.UnsafeEnabled { file_connect_collide_v1_collide_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ImportRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_connect_collide_v1_collide_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ImportResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_connect_collide_v1_collide_proto_rawDesc, NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 1, }, GoTypes: file_connect_collide_v1_collide_proto_goTypes, DependencyIndexes: file_connect_collide_v1_collide_proto_depIdxs, MessageInfos: file_connect_collide_v1_collide_proto_msgTypes, }.Build() File_connect_collide_v1_collide_proto = out.File file_connect_collide_v1_collide_proto_rawDesc = nil file_connect_collide_v1_collide_proto_goTypes = nil file_connect_collide_v1_collide_proto_depIdxs = nil } connect-go-1.13.0/internal/gen/connect/collide/v1/collidev1connect/000077500000000000000000000000001453471351600250545ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/collide/v1/collidev1connect/collide.connect.go000066400000000000000000000130041453471351600304440ustar00rootroot00000000000000// Copyright 2021-2023 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-connect-go. DO NOT EDIT. // // Source: connect/collide/v1/collide.proto package collidev1connect import ( connect "connectrpc.com/connect" v1 "connectrpc.com/connect/internal/gen/connect/collide/v1" context "context" errors "errors" http "net/http" strings "strings" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // CollideServiceName is the fully-qualified name of the CollideService service. CollideServiceName = "connect.collide.v1.CollideService" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // CollideServiceImportProcedure is the fully-qualified name of the CollideService's Import RPC. CollideServiceImportProcedure = "/connect.collide.v1.CollideService/Import" ) // These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. var ( collideServiceServiceDescriptor = v1.File_connect_collide_v1_collide_proto.Services().ByName("CollideService") collideServiceImportMethodDescriptor = collideServiceServiceDescriptor.Methods().ByName("Import") ) // CollideServiceClient is a client for the connect.collide.v1.CollideService service. type CollideServiceClient interface { Import(context.Context, *connect.Request[v1.ImportRequest]) (*connect.Response[v1.ImportResponse], error) } // NewCollideServiceClient constructs a client for the connect.collide.v1.CollideService service. By // default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, // and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the // connect.WithGRPC() or connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewCollideServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) CollideServiceClient { baseURL = strings.TrimRight(baseURL, "/") return &collideServiceClient{ _import: connect.NewClient[v1.ImportRequest, v1.ImportResponse]( httpClient, baseURL+CollideServiceImportProcedure, connect.WithSchema(collideServiceImportMethodDescriptor), connect.WithClientOptions(opts...), ), } } // collideServiceClient implements CollideServiceClient. type collideServiceClient struct { _import *connect.Client[v1.ImportRequest, v1.ImportResponse] } // Import calls connect.collide.v1.CollideService.Import. func (c *collideServiceClient) Import(ctx context.Context, req *connect.Request[v1.ImportRequest]) (*connect.Response[v1.ImportResponse], error) { return c._import.CallUnary(ctx, req) } // CollideServiceHandler is an implementation of the connect.collide.v1.CollideService service. type CollideServiceHandler interface { Import(context.Context, *connect.Request[v1.ImportRequest]) (*connect.Response[v1.ImportResponse], error) } // NewCollideServiceHandler builds an HTTP handler from the service implementation. It returns the // path on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewCollideServiceHandler(svc CollideServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { collideServiceImportHandler := connect.NewUnaryHandler( CollideServiceImportProcedure, svc.Import, connect.WithSchema(collideServiceImportMethodDescriptor), connect.WithHandlerOptions(opts...), ) return "/connect.collide.v1.CollideService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case CollideServiceImportProcedure: collideServiceImportHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedCollideServiceHandler returns CodeUnimplemented from all methods. type UnimplementedCollideServiceHandler struct{} func (UnimplementedCollideServiceHandler) Import(context.Context, *connect.Request[v1.ImportRequest]) (*connect.Response[v1.ImportResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("connect.collide.v1.CollideService.Import is not implemented")) } connect-go-1.13.0/internal/gen/connect/import/000077500000000000000000000000001453471351600211715ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/import/v1/000077500000000000000000000000001453471351600215175ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/import/v1/import.pb.go000066400000000000000000000101471453471351600237630ustar00rootroot00000000000000// Copyright 2021-2023 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.31.0 // protoc (unknown) // source: connect/import/v1/import.proto package importv1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" ) 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) ) var File_connect_import_v1_import_proto protoreflect.FileDescriptor var file_connect_import_v1_import_proto_rawDesc = []byte{ 0x0a, 0x1e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x76, 0x31, 0x32, 0x0f, 0x0a, 0x0d, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0xca, 0x01, 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x49, 0x58, 0xaa, 0x02, 0x11, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x11, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5c, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1d, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5c, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x3a, 0x3a, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var file_connect_import_v1_import_proto_goTypes = []interface{}{} var file_connect_import_v1_import_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_connect_import_v1_import_proto_init() } func file_connect_import_v1_import_proto_init() { if File_connect_import_v1_import_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_connect_import_v1_import_proto_rawDesc, NumEnums: 0, NumMessages: 0, NumExtensions: 0, NumServices: 1, }, GoTypes: file_connect_import_v1_import_proto_goTypes, DependencyIndexes: file_connect_import_v1_import_proto_depIdxs, }.Build() File_connect_import_v1_import_proto = out.File file_connect_import_v1_import_proto_rawDesc = nil file_connect_import_v1_import_proto_goTypes = nil file_connect_import_v1_import_proto_depIdxs = nil } connect-go-1.13.0/internal/gen/connect/import/v1/importv1connect/000077500000000000000000000000001453471351600246525ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/import/v1/importv1connect/import.connect.go000066400000000000000000000065331453471351600301520ustar00rootroot00000000000000// Copyright 2021-2023 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-connect-go. DO NOT EDIT. // // Source: connect/import/v1/import.proto package importv1connect import ( connect "connectrpc.com/connect" v1 "connectrpc.com/connect/internal/gen/connect/import/v1" http "net/http" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // ImportServiceName is the fully-qualified name of the ImportService service. ImportServiceName = "connect.import.v1.ImportService" ) // These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. var ( importServiceServiceDescriptor = v1.File_connect_import_v1_import_proto.Services().ByName("ImportService") ) // ImportServiceClient is a client for the connect.import.v1.ImportService service. type ImportServiceClient interface { } // NewImportServiceClient constructs a client for the connect.import.v1.ImportService service. By // default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, // and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the // connect.WithGRPC() or connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewImportServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ImportServiceClient { return &importServiceClient{} } // importServiceClient implements ImportServiceClient. type importServiceClient struct { } // ImportServiceHandler is an implementation of the connect.import.v1.ImportService service. type ImportServiceHandler interface { } // NewImportServiceHandler builds an HTTP handler from the service implementation. It returns the // path on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewImportServiceHandler(svc ImportServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { return "/connect.import.v1.ImportService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { default: http.NotFound(w, r) } }) } // UnimplementedImportServiceHandler returns CodeUnimplemented from all methods. type UnimplementedImportServiceHandler struct{} connect-go-1.13.0/internal/gen/connect/ping/000077500000000000000000000000001453471351600206145ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/ping/v1/000077500000000000000000000000001453471351600211425ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/ping/v1/ping.pb.go000066400000000000000000000622161453471351600230350ustar00rootroot00000000000000// Copyright 2021-2023 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. // The canonical location for this file is // https://github.com/connectrpc/connect-go/blob/main/internal/proto/connect/ping/v1/ping.proto. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 // protoc (unknown) // source: connect/ping/v1/ping.proto // The connect.ping.v1 package contains an echo service designed to test the // connect-go implementation. package pingv1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) 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) ) type PingRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"` } func (x *PingRequest) Reset() { *x = PingRequest{} if protoimpl.UnsafeEnabled { mi := &file_connect_ping_v1_ping_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *PingRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*PingRequest) ProtoMessage() {} func (x *PingRequest) ProtoReflect() protoreflect.Message { mi := &file_connect_ping_v1_ping_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. func (*PingRequest) Descriptor() ([]byte, []int) { return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{0} } func (x *PingRequest) GetNumber() int64 { if x != nil { return x.Number } return 0 } func (x *PingRequest) GetText() string { if x != nil { return x.Text } return "" } type PingResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"` } func (x *PingResponse) Reset() { *x = PingResponse{} if protoimpl.UnsafeEnabled { mi := &file_connect_ping_v1_ping_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *PingResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*PingResponse) ProtoMessage() {} func (x *PingResponse) ProtoReflect() protoreflect.Message { mi := &file_connect_ping_v1_ping_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PingResponse.ProtoReflect.Descriptor instead. func (*PingResponse) Descriptor() ([]byte, []int) { return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{1} } func (x *PingResponse) GetNumber() int64 { if x != nil { return x.Number } return 0 } func (x *PingResponse) GetText() string { if x != nil { return x.Text } return "" } type FailRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` } func (x *FailRequest) Reset() { *x = FailRequest{} if protoimpl.UnsafeEnabled { mi := &file_connect_ping_v1_ping_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *FailRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*FailRequest) ProtoMessage() {} func (x *FailRequest) ProtoReflect() protoreflect.Message { mi := &file_connect_ping_v1_ping_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use FailRequest.ProtoReflect.Descriptor instead. func (*FailRequest) Descriptor() ([]byte, []int) { return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{2} } func (x *FailRequest) GetCode() int32 { if x != nil { return x.Code } return 0 } type FailResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *FailResponse) Reset() { *x = FailResponse{} if protoimpl.UnsafeEnabled { mi := &file_connect_ping_v1_ping_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *FailResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*FailResponse) ProtoMessage() {} func (x *FailResponse) ProtoReflect() protoreflect.Message { mi := &file_connect_ping_v1_ping_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use FailResponse.ProtoReflect.Descriptor instead. func (*FailResponse) Descriptor() ([]byte, []int) { return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{3} } type SumRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` } func (x *SumRequest) Reset() { *x = SumRequest{} if protoimpl.UnsafeEnabled { mi := &file_connect_ping_v1_ping_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SumRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SumRequest) ProtoMessage() {} func (x *SumRequest) ProtoReflect() protoreflect.Message { mi := &file_connect_ping_v1_ping_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SumRequest.ProtoReflect.Descriptor instead. func (*SumRequest) Descriptor() ([]byte, []int) { return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{4} } func (x *SumRequest) GetNumber() int64 { if x != nil { return x.Number } return 0 } type SumResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Sum int64 `protobuf:"varint,1,opt,name=sum,proto3" json:"sum,omitempty"` } func (x *SumResponse) Reset() { *x = SumResponse{} if protoimpl.UnsafeEnabled { mi := &file_connect_ping_v1_ping_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SumResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*SumResponse) ProtoMessage() {} func (x *SumResponse) ProtoReflect() protoreflect.Message { mi := &file_connect_ping_v1_ping_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SumResponse.ProtoReflect.Descriptor instead. func (*SumResponse) Descriptor() ([]byte, []int) { return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{5} } func (x *SumResponse) GetSum() int64 { if x != nil { return x.Sum } return 0 } type CountUpRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` } func (x *CountUpRequest) Reset() { *x = CountUpRequest{} if protoimpl.UnsafeEnabled { mi := &file_connect_ping_v1_ping_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CountUpRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CountUpRequest) ProtoMessage() {} func (x *CountUpRequest) ProtoReflect() protoreflect.Message { mi := &file_connect_ping_v1_ping_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CountUpRequest.ProtoReflect.Descriptor instead. func (*CountUpRequest) Descriptor() ([]byte, []int) { return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{6} } func (x *CountUpRequest) GetNumber() int64 { if x != nil { return x.Number } return 0 } type CountUpResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` } func (x *CountUpResponse) Reset() { *x = CountUpResponse{} if protoimpl.UnsafeEnabled { mi := &file_connect_ping_v1_ping_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CountUpResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*CountUpResponse) ProtoMessage() {} func (x *CountUpResponse) ProtoReflect() protoreflect.Message { mi := &file_connect_ping_v1_ping_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CountUpResponse.ProtoReflect.Descriptor instead. func (*CountUpResponse) Descriptor() ([]byte, []int) { return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{7} } func (x *CountUpResponse) GetNumber() int64 { if x != nil { return x.Number } return 0 } type CumSumRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` } func (x *CumSumRequest) Reset() { *x = CumSumRequest{} if protoimpl.UnsafeEnabled { mi := &file_connect_ping_v1_ping_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CumSumRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CumSumRequest) ProtoMessage() {} func (x *CumSumRequest) ProtoReflect() protoreflect.Message { mi := &file_connect_ping_v1_ping_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CumSumRequest.ProtoReflect.Descriptor instead. func (*CumSumRequest) Descriptor() ([]byte, []int) { return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{8} } func (x *CumSumRequest) GetNumber() int64 { if x != nil { return x.Number } return 0 } type CumSumResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Sum int64 `protobuf:"varint,1,opt,name=sum,proto3" json:"sum,omitempty"` } func (x *CumSumResponse) Reset() { *x = CumSumResponse{} if protoimpl.UnsafeEnabled { mi := &file_connect_ping_v1_ping_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CumSumResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*CumSumResponse) ProtoMessage() {} func (x *CumSumResponse) ProtoReflect() protoreflect.Message { mi := &file_connect_ping_v1_ping_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CumSumResponse.ProtoReflect.Descriptor instead. func (*CumSumResponse) Descriptor() ([]byte, []int) { return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{9} } func (x *CumSumResponse) GetSum() int64 { if x != nil { return x.Sum } return 0 } var File_connect_ping_v1_ping_proto protoreflect.FileDescriptor var file_connect_ping_v1_ping_proto_rawDesc = []byte{ 0x0a, 0x1a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x22, 0x39, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x22, 0x3a, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x22, 0x21, 0x0a, 0x0b, 0x46, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x0e, 0x0a, 0x0c, 0x46, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x0a, 0x0a, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x1f, 0x0a, 0x0b, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x73, 0x75, 0x6d, 0x22, 0x28, 0x0a, 0x0e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x29, 0x0a, 0x0f, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x0d, 0x43, 0x75, 0x6d, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x22, 0x0a, 0x0e, 0x43, 0x75, 0x6d, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x73, 0x75, 0x6d, 0x32, 0x87, 0x03, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x48, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x45, 0x0a, 0x04, 0x46, 0x61, 0x69, 0x6c, 0x12, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x03, 0x53, 0x75, 0x6d, 0x12, 0x1b, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x12, 0x50, 0x0a, 0x07, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x4f, 0x0a, 0x06, 0x43, 0x75, 0x6d, 0x53, 0x75, 0x6d, 0x12, 0x1e, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x6d, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x6d, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0xba, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x69, 0x6e, 0x67, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5c, 0x50, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1b, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5c, 0x50, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x3a, 0x3a, 0x50, 0x69, 0x6e, 0x67, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_connect_ping_v1_ping_proto_rawDescOnce sync.Once file_connect_ping_v1_ping_proto_rawDescData = file_connect_ping_v1_ping_proto_rawDesc ) func file_connect_ping_v1_ping_proto_rawDescGZIP() []byte { file_connect_ping_v1_ping_proto_rawDescOnce.Do(func() { file_connect_ping_v1_ping_proto_rawDescData = protoimpl.X.CompressGZIP(file_connect_ping_v1_ping_proto_rawDescData) }) return file_connect_ping_v1_ping_proto_rawDescData } var file_connect_ping_v1_ping_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_connect_ping_v1_ping_proto_goTypes = []interface{}{ (*PingRequest)(nil), // 0: connect.ping.v1.PingRequest (*PingResponse)(nil), // 1: connect.ping.v1.PingResponse (*FailRequest)(nil), // 2: connect.ping.v1.FailRequest (*FailResponse)(nil), // 3: connect.ping.v1.FailResponse (*SumRequest)(nil), // 4: connect.ping.v1.SumRequest (*SumResponse)(nil), // 5: connect.ping.v1.SumResponse (*CountUpRequest)(nil), // 6: connect.ping.v1.CountUpRequest (*CountUpResponse)(nil), // 7: connect.ping.v1.CountUpResponse (*CumSumRequest)(nil), // 8: connect.ping.v1.CumSumRequest (*CumSumResponse)(nil), // 9: connect.ping.v1.CumSumResponse } var file_connect_ping_v1_ping_proto_depIdxs = []int32{ 0, // 0: connect.ping.v1.PingService.Ping:input_type -> connect.ping.v1.PingRequest 2, // 1: connect.ping.v1.PingService.Fail:input_type -> connect.ping.v1.FailRequest 4, // 2: connect.ping.v1.PingService.Sum:input_type -> connect.ping.v1.SumRequest 6, // 3: connect.ping.v1.PingService.CountUp:input_type -> connect.ping.v1.CountUpRequest 8, // 4: connect.ping.v1.PingService.CumSum:input_type -> connect.ping.v1.CumSumRequest 1, // 5: connect.ping.v1.PingService.Ping:output_type -> connect.ping.v1.PingResponse 3, // 6: connect.ping.v1.PingService.Fail:output_type -> connect.ping.v1.FailResponse 5, // 7: connect.ping.v1.PingService.Sum:output_type -> connect.ping.v1.SumResponse 7, // 8: connect.ping.v1.PingService.CountUp:output_type -> connect.ping.v1.CountUpResponse 9, // 9: connect.ping.v1.PingService.CumSum:output_type -> connect.ping.v1.CumSumResponse 5, // [5:10] is the sub-list for method output_type 0, // [0:5] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_connect_ping_v1_ping_proto_init() } func file_connect_ping_v1_ping_proto_init() { if File_connect_ping_v1_ping_proto != nil { return } if !protoimpl.UnsafeEnabled { file_connect_ping_v1_ping_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PingRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_connect_ping_v1_ping_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PingResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_connect_ping_v1_ping_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*FailRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_connect_ping_v1_ping_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*FailResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_connect_ping_v1_ping_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SumRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_connect_ping_v1_ping_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SumResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_connect_ping_v1_ping_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CountUpRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_connect_ping_v1_ping_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CountUpResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_connect_ping_v1_ping_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CumSumRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_connect_ping_v1_ping_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CumSumResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_connect_ping_v1_ping_proto_rawDesc, NumEnums: 0, NumMessages: 10, NumExtensions: 0, NumServices: 1, }, GoTypes: file_connect_ping_v1_ping_proto_goTypes, DependencyIndexes: file_connect_ping_v1_ping_proto_depIdxs, MessageInfos: file_connect_ping_v1_ping_proto_msgTypes, }.Build() File_connect_ping_v1_ping_proto = out.File file_connect_ping_v1_ping_proto_rawDesc = nil file_connect_ping_v1_ping_proto_goTypes = nil file_connect_ping_v1_ping_proto_depIdxs = nil } connect-go-1.13.0/internal/gen/connect/ping/v1/pingv1connect/000077500000000000000000000000001453471351600237205ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connect/ping/v1/pingv1connect/ping.connect.go000066400000000000000000000300761453471351600266420ustar00rootroot00000000000000// Copyright 2021-2023 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. // The canonical location for this file is // https://github.com/connectrpc/connect-go/blob/main/internal/proto/connect/ping/v1/ping.proto. // Code generated by protoc-gen-connect-go. DO NOT EDIT. // // Source: connect/ping/v1/ping.proto // The connect.ping.v1 package contains an echo service designed to test the // connect-go implementation. package pingv1connect import ( connect "connectrpc.com/connect" v1 "connectrpc.com/connect/internal/gen/connect/ping/v1" context "context" errors "errors" http "net/http" strings "strings" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // PingServiceName is the fully-qualified name of the PingService service. PingServiceName = "connect.ping.v1.PingService" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // PingServicePingProcedure is the fully-qualified name of the PingService's Ping RPC. PingServicePingProcedure = "/connect.ping.v1.PingService/Ping" // PingServiceFailProcedure is the fully-qualified name of the PingService's Fail RPC. PingServiceFailProcedure = "/connect.ping.v1.PingService/Fail" // PingServiceSumProcedure is the fully-qualified name of the PingService's Sum RPC. PingServiceSumProcedure = "/connect.ping.v1.PingService/Sum" // PingServiceCountUpProcedure is the fully-qualified name of the PingService's CountUp RPC. PingServiceCountUpProcedure = "/connect.ping.v1.PingService/CountUp" // PingServiceCumSumProcedure is the fully-qualified name of the PingService's CumSum RPC. PingServiceCumSumProcedure = "/connect.ping.v1.PingService/CumSum" ) // These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. var ( pingServiceServiceDescriptor = v1.File_connect_ping_v1_ping_proto.Services().ByName("PingService") pingServicePingMethodDescriptor = pingServiceServiceDescriptor.Methods().ByName("Ping") pingServiceFailMethodDescriptor = pingServiceServiceDescriptor.Methods().ByName("Fail") pingServiceSumMethodDescriptor = pingServiceServiceDescriptor.Methods().ByName("Sum") pingServiceCountUpMethodDescriptor = pingServiceServiceDescriptor.Methods().ByName("CountUp") pingServiceCumSumMethodDescriptor = pingServiceServiceDescriptor.Methods().ByName("CumSum") ) // PingServiceClient is a client for the connect.ping.v1.PingService service. type PingServiceClient interface { // Ping sends a ping to the server to determine if it's reachable. Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) // Fail always fails. Fail(context.Context, *connect.Request[v1.FailRequest]) (*connect.Response[v1.FailResponse], error) // Sum calculates the sum of the numbers sent on the stream. Sum(context.Context) *connect.ClientStreamForClient[v1.SumRequest, v1.SumResponse] // CountUp returns a stream of the numbers up to the given request. CountUp(context.Context, *connect.Request[v1.CountUpRequest]) (*connect.ServerStreamForClient[v1.CountUpResponse], error) // CumSum determines the cumulative sum of all the numbers sent on the stream. CumSum(context.Context) *connect.BidiStreamForClient[v1.CumSumRequest, v1.CumSumResponse] } // NewPingServiceClient constructs a client for the connect.ping.v1.PingService service. By default, // it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and // sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() // or connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewPingServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PingServiceClient { baseURL = strings.TrimRight(baseURL, "/") return &pingServiceClient{ ping: connect.NewClient[v1.PingRequest, v1.PingResponse]( httpClient, baseURL+PingServicePingProcedure, connect.WithSchema(pingServicePingMethodDescriptor), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithClientOptions(opts...), ), fail: connect.NewClient[v1.FailRequest, v1.FailResponse]( httpClient, baseURL+PingServiceFailProcedure, connect.WithSchema(pingServiceFailMethodDescriptor), connect.WithClientOptions(opts...), ), sum: connect.NewClient[v1.SumRequest, v1.SumResponse]( httpClient, baseURL+PingServiceSumProcedure, connect.WithSchema(pingServiceSumMethodDescriptor), connect.WithClientOptions(opts...), ), countUp: connect.NewClient[v1.CountUpRequest, v1.CountUpResponse]( httpClient, baseURL+PingServiceCountUpProcedure, connect.WithSchema(pingServiceCountUpMethodDescriptor), connect.WithClientOptions(opts...), ), cumSum: connect.NewClient[v1.CumSumRequest, v1.CumSumResponse]( httpClient, baseURL+PingServiceCumSumProcedure, connect.WithSchema(pingServiceCumSumMethodDescriptor), connect.WithClientOptions(opts...), ), } } // pingServiceClient implements PingServiceClient. type pingServiceClient struct { ping *connect.Client[v1.PingRequest, v1.PingResponse] fail *connect.Client[v1.FailRequest, v1.FailResponse] sum *connect.Client[v1.SumRequest, v1.SumResponse] countUp *connect.Client[v1.CountUpRequest, v1.CountUpResponse] cumSum *connect.Client[v1.CumSumRequest, v1.CumSumResponse] } // Ping calls connect.ping.v1.PingService.Ping. func (c *pingServiceClient) Ping(ctx context.Context, req *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) { return c.ping.CallUnary(ctx, req) } // Fail calls connect.ping.v1.PingService.Fail. func (c *pingServiceClient) Fail(ctx context.Context, req *connect.Request[v1.FailRequest]) (*connect.Response[v1.FailResponse], error) { return c.fail.CallUnary(ctx, req) } // Sum calls connect.ping.v1.PingService.Sum. func (c *pingServiceClient) Sum(ctx context.Context) *connect.ClientStreamForClient[v1.SumRequest, v1.SumResponse] { return c.sum.CallClientStream(ctx) } // CountUp calls connect.ping.v1.PingService.CountUp. func (c *pingServiceClient) CountUp(ctx context.Context, req *connect.Request[v1.CountUpRequest]) (*connect.ServerStreamForClient[v1.CountUpResponse], error) { return c.countUp.CallServerStream(ctx, req) } // CumSum calls connect.ping.v1.PingService.CumSum. func (c *pingServiceClient) CumSum(ctx context.Context) *connect.BidiStreamForClient[v1.CumSumRequest, v1.CumSumResponse] { return c.cumSum.CallBidiStream(ctx) } // PingServiceHandler is an implementation of the connect.ping.v1.PingService service. type PingServiceHandler interface { // Ping sends a ping to the server to determine if it's reachable. Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) // Fail always fails. Fail(context.Context, *connect.Request[v1.FailRequest]) (*connect.Response[v1.FailResponse], error) // Sum calculates the sum of the numbers sent on the stream. Sum(context.Context, *connect.ClientStream[v1.SumRequest]) (*connect.Response[v1.SumResponse], error) // CountUp returns a stream of the numbers up to the given request. CountUp(context.Context, *connect.Request[v1.CountUpRequest], *connect.ServerStream[v1.CountUpResponse]) error // CumSum determines the cumulative sum of all the numbers sent on the stream. CumSum(context.Context, *connect.BidiStream[v1.CumSumRequest, v1.CumSumResponse]) error } // NewPingServiceHandler builds an HTTP handler from the service implementation. It returns the path // on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewPingServiceHandler(svc PingServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { pingServicePingHandler := connect.NewUnaryHandler( PingServicePingProcedure, svc.Ping, connect.WithSchema(pingServicePingMethodDescriptor), connect.WithIdempotency(connect.IdempotencyNoSideEffects), connect.WithHandlerOptions(opts...), ) pingServiceFailHandler := connect.NewUnaryHandler( PingServiceFailProcedure, svc.Fail, connect.WithSchema(pingServiceFailMethodDescriptor), connect.WithHandlerOptions(opts...), ) pingServiceSumHandler := connect.NewClientStreamHandler( PingServiceSumProcedure, svc.Sum, connect.WithSchema(pingServiceSumMethodDescriptor), connect.WithHandlerOptions(opts...), ) pingServiceCountUpHandler := connect.NewServerStreamHandler( PingServiceCountUpProcedure, svc.CountUp, connect.WithSchema(pingServiceCountUpMethodDescriptor), connect.WithHandlerOptions(opts...), ) pingServiceCumSumHandler := connect.NewBidiStreamHandler( PingServiceCumSumProcedure, svc.CumSum, connect.WithSchema(pingServiceCumSumMethodDescriptor), connect.WithHandlerOptions(opts...), ) return "/connect.ping.v1.PingService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case PingServicePingProcedure: pingServicePingHandler.ServeHTTP(w, r) case PingServiceFailProcedure: pingServiceFailHandler.ServeHTTP(w, r) case PingServiceSumProcedure: pingServiceSumHandler.ServeHTTP(w, r) case PingServiceCountUpProcedure: pingServiceCountUpHandler.ServeHTTP(w, r) case PingServiceCumSumProcedure: pingServiceCumSumHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedPingServiceHandler returns CodeUnimplemented from all methods. type UnimplementedPingServiceHandler struct{} func (UnimplementedPingServiceHandler) Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.Ping is not implemented")) } func (UnimplementedPingServiceHandler) Fail(context.Context, *connect.Request[v1.FailRequest]) (*connect.Response[v1.FailResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.Fail is not implemented")) } func (UnimplementedPingServiceHandler) Sum(context.Context, *connect.ClientStream[v1.SumRequest]) (*connect.Response[v1.SumResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.Sum is not implemented")) } func (UnimplementedPingServiceHandler) CountUp(context.Context, *connect.Request[v1.CountUpRequest], *connect.ServerStream[v1.CountUpResponse]) error { return connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.CountUp is not implemented")) } func (UnimplementedPingServiceHandler) CumSum(context.Context, *connect.BidiStream[v1.CumSumRequest, v1.CumSumResponse]) error { return connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.CumSum is not implemented")) } connect-go-1.13.0/internal/gen/connectext/000077500000000000000000000000001453471351600204005ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connectext/grpc/000077500000000000000000000000001453471351600213335ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connectext/grpc/status/000077500000000000000000000000001453471351600226565ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connectext/grpc/status/v1/000077500000000000000000000000001453471351600232045ustar00rootroot00000000000000connect-go-1.13.0/internal/gen/connectext/grpc/status/v1/status.pb.go000066400000000000000000000177561453471351600254760ustar00rootroot00000000000000// Copyright 2021-2023 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.31.0 // 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" ) 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 sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields 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"` } func (x *Status) Reset() { *x = Status{} if protoimpl.UnsafeEnabled { 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 protoimpl.UnsafeEnabled && 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 var file_connectext_grpc_status_v1_status_proto_rawDesc = []byte{ 0x0a, 0x26, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x76, 0x31, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x66, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x42, 0xc3, 0x01, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x46, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x47, 0x53, 0x58, 0xaa, 0x02, 0x0e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0e, 0x47, 0x72, 0x70, 0x63, 0x5c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1a, 0x47, 0x72, 0x70, 0x63, 0x5c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x10, 0x47, 0x72, 0x70, 0x63, 0x3a, 0x3a, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_connectext_grpc_status_v1_status_proto_rawDescOnce sync.Once file_connectext_grpc_status_v1_status_proto_rawDescData = file_connectext_grpc_status_v1_status_proto_rawDesc ) 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(file_connectext_grpc_status_v1_status_proto_rawDescData) }) 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 = []interface{}{ (*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 } if !protoimpl.UnsafeEnabled { file_connectext_grpc_status_v1_status_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Status); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: 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_rawDesc = nil file_connectext_grpc_status_v1_status_proto_goTypes = nil file_connectext_grpc_status_v1_status_proto_depIdxs = nil } connect-go-1.13.0/internal/memhttp/000077500000000000000000000000001453471351600171335ustar00rootroot00000000000000connect-go-1.13.0/internal/memhttp/listener.go000066400000000000000000000045461453471351600213200ustar00rootroot00000000000000// Copyright 2021-2023 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 memhttp import ( "context" "errors" "net" "sync" ) var ( errListenerClosed = errors.New("listener closed") ) // memoryListener is a net.Listener that listens on an in memory network. type memoryListener struct { addr memoryAddr conns chan net.Conn once sync.Once closed chan struct{} } // newMemoryListener returns a new in-memory listener. func newMemoryListener(addr string) *memoryListener { return &memoryListener{ addr: memoryAddr(addr), conns: make(chan net.Conn), closed: make(chan struct{}), } } // Accept implements net.Listener. func (l *memoryListener) Accept() (net.Conn, error) { select { case <-l.closed: return nil, &net.OpError{ Op: "accept", Net: l.addr.Network(), Addr: l.addr, Err: errListenerClosed, } case server := <-l.conns: return server, nil } } // Close implements net.Listener. func (l *memoryListener) Close() error { l.once.Do(func() { close(l.closed) }) return nil } // Addr implements net.Listener. func (l *memoryListener) Addr() net.Addr { return l.addr } // DialContext is the type expected by http.Transport.DialContext. func (l *memoryListener) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { server, client := net.Pipe() select { case <-ctx.Done(): return nil, &net.OpError{Op: "dial", Net: l.addr.Network(), Err: ctx.Err()} case l.conns <- server: return client, nil case <-l.closed: return nil, &net.OpError{Op: "dial", Net: l.addr.Network(), Err: errListenerClosed} } } type memoryAddr string // Network implements net.Addr. func (memoryAddr) Network() string { return "memory" } // String implements io.Stringer, returning a value that matches the // certificates used by net/http/httptest. func (a memoryAddr) String() string { return string(a) } connect-go-1.13.0/internal/memhttp/memhttp.go000066400000000000000000000105741453471351600211470ustar00rootroot00000000000000// Copyright 2021-2023 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 memhttp import ( "context" "crypto/tls" "errors" "net" "net/http" "sync" "time" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) // Server is a net/http server that uses in-memory pipes instead of TCP. By // default, it supports http/2 via h2c. It otherwise uses the same configuration // as the zero value of [http.Server]. type Server struct { server http.Server listener *memoryListener url string cleanupTimeout time.Duration serverWG sync.WaitGroup serverErr error } // NewServer creates a new Server that uses the given handler. Configuration // options may be provided via [Option]s. func NewServer(handler http.Handler, opts ...Option) *Server { var cfg config WithCleanupTimeout(5 * time.Second).apply(&cfg) for _, opt := range opts { opt.apply(&cfg) } h2s := &http2.Server{} handler = h2c.NewHandler(handler, h2s) listener := newMemoryListener("1.2.3.4") // httptest.DefaultRemoteAddr server := &Server{ server: http.Server{ Handler: handler, ReadHeaderTimeout: 5 * time.Second, }, listener: listener, url: "http://" + listener.Addr().String(), cleanupTimeout: cfg.CleanupTimeout, } server.serverWG.Add(1) go func() { defer server.serverWG.Done() server.serverErr = server.server.Serve(server.listener) }() return server } // Transport returns a [http2.Transport] configured to use in-memory pipes // rather than TCP and speak both HTTP/1.1 and HTTP/2. // // Callers may reconfigure the returned transport without affecting other transports. func (s *Server) Transport() *http2.Transport { return &http2.Transport{ DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { return s.listener.DialContext(ctx, network, addr) }, AllowHTTP: true, } } // TransportHTTP1 returns a [http.Transport] configured to use in-memory pipes // rather than TCP and speak HTTP/1.1. // // Callers may reconfigure the returned transport without affecting other transports. func (s *Server) TransportHTTP1() *http.Transport { return &http.Transport{ DialContext: s.listener.DialContext, // TODO(emcfarlane): DisableKeepAlives false can causes tests // to hang on shutdown. DisableKeepAlives: true, } } // Client returns an [http.Client] configured to use in-memory pipes rather // than TCP and speak HTTP/2. It is configured to use the same // [http2.Transport] as [Transport]. // // Callers may reconfigure the returned client without affecting other clients. func (s *Server) Client() *http.Client { return &http.Client{Transport: s.Transport()} } // URL returns the server's URL. func (s *Server) URL() string { return s.url } // Shutdown gracefully shuts down the server, without interrupting any active // connections. See [http.Server.Shutdown] for details. func (s *Server) Shutdown(ctx context.Context) error { if err := s.server.Shutdown(ctx); err != nil { return err } return s.Wait() } // Cleanup calls shutdown with a background context set with the cleanup timeout. // The default timeout duration is 5 seconds. func (s *Server) Cleanup() error { ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, s.cleanupTimeout) defer cancel() return s.Shutdown(ctx) } // Close closes the server's listener. It does not wait for connections to // finish. func (s *Server) Close() error { return s.server.Close() } // RegisterOnShutdown registers a function to call on Shutdown. See // [http.Server.RegisterOnShutdown] for details. func (s *Server) RegisterOnShutdown(f func()) { s.server.RegisterOnShutdown(f) } // Wait blocks until the server exits, then returns an error if not // a [http.ErrServerClosed] error. func (s *Server) Wait() error { s.serverWG.Wait() if !errors.Is(s.serverErr, http.ErrServerClosed) { return s.serverErr } return nil } connect-go-1.13.0/internal/memhttp/memhttp_test.go000066400000000000000000000067551453471351600222140ustar00rootroot00000000000000// Copyright 2021-2023 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 memhttp_test import ( "context" "fmt" "io" "net/http" "runtime" "sync" "testing" "time" "connectrpc.com/connect/internal/assert" "connectrpc.com/connect/internal/memhttp" "connectrpc.com/connect/internal/memhttp/memhttptest" ) func TestServerTransport(t *testing.T) { t.Parallel() concurrency := runtime.GOMAXPROCS(0) * 8 const greeting = "Hello, world!" handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(greeting)) }) server := memhttptest.NewServer(t, handler) for _, transport := range []http.RoundTripper{ server.Transport(), server.TransportHTTP1(), } { client := &http.Client{Transport: transport} t.Run(fmt.Sprintf("%T", transport), func(t *testing.T) { t.Parallel() var wg sync.WaitGroup for i := 0; i < concurrency; i++ { wg.Add(1) go func() { defer wg.Done() req, err := http.NewRequestWithContext( context.Background(), http.MethodGet, server.URL(), nil, ) assert.Nil(t, err) res, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, res.StatusCode, http.StatusOK) body, err := io.ReadAll(res.Body) assert.Nil(t, err) assert.Nil(t, res.Body.Close()) assert.Equal(t, string(body), greeting) }() } wg.Wait() }) } } func TestRegisterOnShutdown(t *testing.T) { t.Parallel() okay := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) server := memhttp.NewServer(okay) done := make(chan struct{}) server.RegisterOnShutdown(func() { close(done) }) assert.Nil(t, server.Shutdown(context.Background())) select { case <-done: case <-time.After(5 * time.Second): t.Error("OnShutdown hook didn't fire") } } func Example() { hello := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, "Hello, world!") }) srv := memhttp.NewServer(hello) defer srv.Close() res, err := srv.Client().Get(srv.URL()) if err != nil { panic(err) } defer res.Body.Close() fmt.Println(res.Status) // Output: // 200 OK } func ExampleServer_Client() { hello := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, "Hello, world!") }) srv := memhttp.NewServer(hello) defer srv.Close() client := srv.Client() client.Timeout = 10 * time.Second res, err := client.Get(srv.URL()) if err != nil { panic(err) } defer res.Body.Close() fmt.Println(res.Status) // Output: // 200 OK } func ExampleServer_Shutdown() { hello := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, "Hello, world!") }) srv := memhttp.NewServer(hello) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { panic(err) } fmt.Println("Server has shut down") // Output: // Server has shut down } connect-go-1.13.0/internal/memhttp/memhttptest/000077500000000000000000000000001453471351600215115ustar00rootroot00000000000000connect-go-1.13.0/internal/memhttp/memhttptest/http.go000066400000000000000000000032741453471351600230250ustar00rootroot00000000000000// Copyright 2021-2023 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 memhttptest import ( "log" "net/http" "testing" "connectrpc.com/connect/internal/memhttp" ) // NewServer constructs a [memhttp.Server] with defaults suitable for tests: // it logs runtime errors to the provided testing.TB, and it automatically shuts // down the server when the test completes. Startup and shutdown errors fail the // test. // // To customize the server, use any [memhttp.Option]. In particular, it may be // necessary to customize the shutdown timeout with // [memhttp.WithCleanupTimeout]. func NewServer(tb testing.TB, handler http.Handler, opts ...memhttp.Option) *memhttp.Server { tb.Helper() logger := log.New(&testWriter{tb}, "" /* prefix */, log.Lshortfile) opts = append([]memhttp.Option{memhttp.WithErrorLog(logger)}, opts...) server := memhttp.NewServer(handler, opts...) tb.Cleanup(func() { if err := server.Cleanup(); err != nil { tb.Error(err) } }) return server } // testWriter is an io.Writer that logs to the testing.TB. type testWriter struct { tb testing.TB } func (l *testWriter) Write(p []byte) (int, error) { l.tb.Log(string(p)) return len(p), nil } connect-go-1.13.0/internal/memhttp/option.go000066400000000000000000000027301453471351600207740ustar00rootroot00000000000000// Copyright 2021-2023 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 memhttp import ( "log" "time" ) // config is the configuration for a Server. type config struct { CleanupTimeout time.Duration ErrorLog *log.Logger } // An Option configures a Server. type Option interface { apply(*config) } type optionFunc func(*config) func (f optionFunc) apply(cfg *config) { f(cfg) } // WithOptions composes multiple Options into one. func WithOptions(opts ...Option) Option { return optionFunc(func(cfg *config) { for _, opt := range opts { opt.apply(cfg) } }) } // WithErrorLog sets [http.Server.ErrorLog]. func WithErrorLog(l *log.Logger) Option { return optionFunc(func(cfg *config) { cfg.ErrorLog = l }) } // WithCleanupTimeout customizes the default five-second timeout for the // server's Cleanup method. func WithCleanupTimeout(d time.Duration) Option { return optionFunc(func(cfg *config) { cfg.CleanupTimeout = d }) } connect-go-1.13.0/internal/proto/000077500000000000000000000000001453471351600166205ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/buf.yaml000066400000000000000000000004521453471351600202610ustar00rootroot00000000000000version: v1 lint: use: - DEFAULT ignore: # We don't control these definitions, so we ignore lint errors. - connectext/grpc/health/v1/health.proto - connectext/grpc/reflection/v1alpha/reflection.proto - connectext/grpc/status/v1/status.proto breaking: use: - WIRE_JSON connect-go-1.13.0/internal/proto/connect/000077500000000000000000000000001453471351600202515ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connect/collide/000077500000000000000000000000001453471351600216645ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connect/collide/v1/000077500000000000000000000000001453471351600222125ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connect/collide/v1/collide.proto000066400000000000000000000014241453471351600247130ustar00rootroot00000000000000// Copyright 2021-2023 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. syntax = "proto3"; package connect.collide.v1; message ImportRequest {} message ImportResponse {} service CollideService { rpc Import(ImportRequest) returns (ImportResponse) {} } connect-go-1.13.0/internal/proto/connect/import/000077500000000000000000000000001453471351600215635ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connect/import/v1/000077500000000000000000000000001453471351600221115ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connect/import/v1/import.proto000066400000000000000000000012441453471351600245110ustar00rootroot00000000000000// Copyright 2021-2023 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. syntax = "proto3"; package connect.import.v1; service ImportService {} connect-go-1.13.0/internal/proto/connect/ping/000077500000000000000000000000001453471351600212065ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connect/ping/v1/000077500000000000000000000000001453471351600215345ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connect/ping/v1/ping.proto000066400000000000000000000037701453471351600235650ustar00rootroot00000000000000// Copyright 2021-2023 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. // The canonical location for this file is // https://github.com/connectrpc/connect-go/blob/main/internal/proto/connect/ping/v1/ping.proto. syntax = "proto3"; // The connect.ping.v1 package contains an echo service designed to test the // connect-go implementation. package connect.ping.v1; message PingRequest { int64 number = 1; string text = 2; } message PingResponse { int64 number = 1; string text = 2; } message FailRequest { int32 code = 1; } message FailResponse {} message SumRequest { int64 number = 1; } message SumResponse { int64 sum = 1; } message CountUpRequest { int64 number = 1; } message CountUpResponse { int64 number = 1; } message CumSumRequest { int64 number = 1; } message CumSumResponse { int64 sum = 1; } service PingService { // Ping sends a ping to the server to determine if it's reachable. rpc Ping(PingRequest) returns (PingResponse) { option idempotency_level = NO_SIDE_EFFECTS; } // Fail always fails. rpc Fail(FailRequest) returns (FailResponse) {} // Sum calculates the sum of the numbers sent on the stream. rpc Sum(stream SumRequest) returns (SumResponse) {} // CountUp returns a stream of the numbers up to the given request. rpc CountUp(CountUpRequest) returns (stream CountUpResponse) {} // CumSum determines the cumulative sum of all the numbers sent on the stream. rpc CumSum(stream CumSumRequest) returns (stream CumSumResponse) {} } connect-go-1.13.0/internal/proto/connectext/000077500000000000000000000000001453471351600207725ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connectext/grpc/000077500000000000000000000000001453471351600217255ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connectext/grpc/status/000077500000000000000000000000001453471351600232505ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connectext/grpc/status/v1/000077500000000000000000000000001453471351600235765ustar00rootroot00000000000000connect-go-1.13.0/internal/proto/connectext/grpc/status/v1/status.proto000066400000000000000000000022261453471351600262100ustar00rootroot00000000000000// Copyright 2021-2023 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. syntax = "proto3"; // This package is for internal use by Connect, and provides no backward // compatibility guarantees whatsoever. package grpc.status.v1; import "google/protobuf/any.proto"; // 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. message Status { int32 code = 1; // a google.rpc.Code string message = 2; // developer-facing, English (localize in details or client-side) repeated google.protobuf.Any details = 3; } connect-go-1.13.0/internal/testdata/000077500000000000000000000000001453471351600172665ustar00rootroot00000000000000connect-go-1.13.0/internal/testdata/server.crt000066400000000000000000000017041453471351600213100ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICmjCCAfygAwIBAgIUGM2+eTbJp3g6o3DPtDQG3tVfL+EwCgYIKoZIzj0EAwIw XzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNh biBGcmFuY2lzY28xDjAMBgNVBAoMBXJlUlBDMRMwEQYDVQQDDApnaXRodWIuY29t MB4XDTIxMDczMDIwMTQzM1oXDTMxMDcyODIwMTQzM1owXzELMAkGA1UEBhMCVVMx EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDjAM BgNVBAoMBXJlUlBDMRMwEQYDVQQDDApnaXRodWIuY29tMIGbMBAGByqGSM49AgEG BSuBBAAjA4GGAAQA4WPD74+AyZAOxAgWo58oC1JUnFy9Ln3A66rWmDPPprCJhIJ9 i5SyXG1NxwMEIGzyFT3Bp4wWru0ogfpTxPClQ/4Aulrqisiyu4C9Ds1DRJg53E8D n/CKsQwUYo7MbZIrn63+77kNlJlKloUfBygZ9vQiLjhNA52A95aWRp5yNna/GvCj UzBRMB0GA1UdDgQWBBRm+gq9izCELNh05BdEH79AWoR9ezAfBgNVHSMEGDAWgBRm +gq9izCELNh05BdEH79AWoR9ezAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMC A4GLADCBhwJCAT6Yj94euijggFrKJMcHNV7OZzFkugqiBzOI4OsjA6NfU0UExxBq VDuUUk2Ek3c4GWYuPvDbdx49Q+zge9Cgl3jYAkF+QrzQWIJHC2L5f5wk8488DBzb vs0nDV9r+drHM1KDd674y/p2sjY04PQgbNgair+BxjWxCc2QQWGw0SaWDLj/Ag== -----END CERTIFICATE----- connect-go-1.13.0/internal/testdata/server.key000066400000000000000000000006641453471351600213140ustar00rootroot00000000000000-----BEGIN EC PARAMETERS----- BgUrgQQAIw== -----END EC PARAMETERS----- -----BEGIN EC PRIVATE KEY----- MIHcAgEBBEIAN9tRNa4oaevnNxwZEkDTIfbcPlfQP49Q4lapXa1TUXu4Olfu6QyW ll3OxNIwDg54nsKSuKoaVBOKQelOJjTzyLqgBwYFK4EEACOhgYkDgYYABADhY8Pv j4DJkA7ECBajnygLUlScXL0ufcDrqtaYM8+msImEgn2LlLJcbU3HAwQgbPIVPcGn jBau7SiB+lPE8KVD/gC6WuqKyLK7gL0OzUNEmDncTwOf8IqxDBRijsxtkiufrf7v uQ2UmUqWhR8HKBn29CIuOE0DnYD3lpZGnnI2dr8a8A== -----END EC PRIVATE KEY----- connect-go-1.13.0/option.go000066400000000000000000000536741453471351600155170ustar00rootroot00000000000000// Copyright 2021-2023 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. // // 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 // [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(min int) Option { return &compressMinBytesOption{Min: min} } // 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(max int) Option { return &readMaxBytesOption{Max: max} } // 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(max int) Option { return &sendMaxBytesOption{Max: max} } // 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) } } connect-go-1.13.0/protobuf_util.go000066400000000000000000000023661453471351600170740ustar00rootroot00000000000000// Copyright 2021-2023 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 } connect-go-1.13.0/protobuf_util_test.go000066400000000000000000000030441453471351600201250ustar00rootroot00000000000000// Copyright 2021-2023 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 ( "testing" "connectrpc.com/connect/internal/assert" ) func TestParseProtobufURL(t *testing.T) { t.Parallel() assertExtractedProtoPath( t, // full URL "https://api.foo.com/grpc/foo.user.v1.UserService/GetUser", "/foo.user.v1.UserService/GetUser", ) assertExtractedProtoPath( t, // rooted path "/foo.user.v1.UserService/GetUser", "/foo.user.v1.UserService/GetUser", ) assertExtractedProtoPath( t, // path without leading or trailing slashes "foo.user.v1.UserService/GetUser", "/foo.user.v1.UserService/GetUser", ) assertExtractedProtoPath( t, // path with trailing slash "/foo.user.v1.UserService.GetUser/", "/foo.user.v1.UserService.GetUser", ) // edge cases assertExtractedProtoPath(t, "", "/") assertExtractedProtoPath(t, "//", "/") } func assertExtractedProtoPath(tb testing.TB, inputURL, expectPath string) { tb.Helper() assert.Equal( tb, extractProtoPath(inputURL), expectPath, ) } connect-go-1.13.0/protocol.go000066400000000000000000000331461453471351600160400ustar00rootroot00000000000000// Copyright 2021-2023 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" 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) } connect-go-1.13.0/protocol_connect.go000066400000000000000000001227321453471351600175510ustar00rootroot00000000000000// Copyright 2021-2023 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/base64" "encoding/json" "errors" "fmt" "io" "math" "net/http" "net/url" "runtime" "strconv" "strings" "time" "google.golang.org/protobuf/types/known/anypb" ) const ( connectUnaryHeaderCompression = "Content-Encoding" connectUnaryHeaderAcceptCompression = "Accept-Encoding" connectUnaryTrailerPrefix = "Trailer-" connectStreamingHeaderCompression = "Connect-Content-Encoding" connectStreamingHeaderAcceptCompression = "Connect-Accept-Encoding" connectHeaderTimeout = "Connect-Timeout-Ms" connectHeaderProtocolVersion = "Connect-Protocol-Version" connectProtocolVersion = "1" headerVary = "Vary" connectFlagEnvelopeEndStream = 0b00000010 connectUnaryContentTypePrefix = "application/" connectUnaryContentTypeJSON = connectUnaryContentTypePrefix + "json" connectStreamingContentTypePrefix = "application/connect+" connectUnaryEncodingQueryParameter = "encoding" connectUnaryMessageQueryParameter = "message" connectUnaryBase64QueryParameter = "base64" connectUnaryCompressionQueryParameter = "compression" connectUnaryConnectQueryParameter = "connect" connectUnaryConnectQueryValue = "v" + connectProtocolVersion ) // defaultConnectUserAgent returns a User-Agent string similar to those used in gRPC. var defaultConnectUserAgent = fmt.Sprintf("connect-go/%s (%s)", Version, runtime.Version()) type protocolConnect struct{} // NewHandler implements protocol, so it must return an interface. func (*protocolConnect) NewHandler(params *protocolHandlerParams) protocolHandler { methods := make(map[string]struct{}) methods[http.MethodPost] = struct{}{} if params.Spec.StreamType == StreamTypeUnary && params.IdempotencyLevel == IdempotencyNoSideEffects { methods[http.MethodGet] = struct{}{} } contentTypes := make(map[string]struct{}) for _, name := range params.Codecs.Names() { if params.Spec.StreamType == StreamTypeUnary { contentTypes[canonicalizeContentType(connectUnaryContentTypePrefix+name)] = struct{}{} continue } contentTypes[canonicalizeContentType(connectStreamingContentTypePrefix+name)] = struct{}{} } return &connectHandler{ protocolHandlerParams: *params, methods: methods, accept: contentTypes, } } // NewClient implements protocol, so it must return an interface. func (*protocolConnect) NewClient(params *protocolClientParams) (protocolClient, error) { return &connectClient{ protocolClientParams: *params, peer: newPeerFromURL(params.URL, ProtocolConnect), }, nil } type connectHandler struct { protocolHandlerParams methods map[string]struct{} accept map[string]struct{} } func (h *connectHandler) Methods() map[string]struct{} { return h.methods } func (h *connectHandler) ContentTypes() map[string]struct{} { return h.accept } func (*connectHandler) SetTimeout(request *http.Request) (context.Context, context.CancelFunc, error) { timeout := getHeaderCanonical(request.Header, connectHeaderTimeout) if timeout == "" { return request.Context(), nil, nil } if len(timeout) > 10 { return nil, nil, errorf(CodeInvalidArgument, "parse timeout: %q has >10 digits", timeout) } millis, err := strconv.ParseInt(timeout, 10 /* base */, 64 /* bitsize */) if err != nil { return nil, nil, errorf(CodeInvalidArgument, "parse timeout: %w", err) } ctx, cancel := context.WithTimeout( request.Context(), time.Duration(millis)*time.Millisecond, ) return ctx, cancel, nil } func (h *connectHandler) CanHandlePayload(request *http.Request, contentType string) bool { if request.Method == http.MethodGet { query := request.URL.Query() codecName := query.Get(connectUnaryEncodingQueryParameter) contentType = connectContentTypeFromCodecName( h.Spec.StreamType, codecName, ) } _, ok := h.accept[contentType] return ok } func (h *connectHandler) NewConn( responseWriter http.ResponseWriter, request *http.Request, ) (handlerConnCloser, bool) { query := request.URL.Query() // We need to parse metadata before entering the interceptor stack; we'll // send the error to the client later on. var contentEncoding, acceptEncoding string if h.Spec.StreamType == StreamTypeUnary { if request.Method == http.MethodGet { contentEncoding = query.Get(connectUnaryCompressionQueryParameter) } else { contentEncoding = getHeaderCanonical(request.Header, connectUnaryHeaderCompression) } acceptEncoding = getHeaderCanonical(request.Header, connectUnaryHeaderAcceptCompression) } else { contentEncoding = getHeaderCanonical(request.Header, connectStreamingHeaderCompression) acceptEncoding = getHeaderCanonical(request.Header, connectStreamingHeaderAcceptCompression) } requestCompression, responseCompression, failed := negotiateCompression( h.CompressionPools, contentEncoding, acceptEncoding, ) if failed == nil { failed = checkServerStreamsCanFlush(h.Spec, responseWriter) } if failed == nil && request.Method == http.MethodGet { version := query.Get(connectUnaryConnectQueryParameter) if version == "" && h.RequireConnectProtocolHeader { failed = errorf(CodeInvalidArgument, "missing required query parameter: set %s to %q", connectUnaryConnectQueryParameter, connectUnaryConnectQueryValue) } else if version != "" && version != connectUnaryConnectQueryValue { failed = errorf(CodeInvalidArgument, "%s must be %q: got %q", connectUnaryConnectQueryParameter, connectUnaryConnectQueryValue, version) } } if failed == nil && request.Method == http.MethodPost { version := getHeaderCanonical(request.Header, connectHeaderProtocolVersion) if version == "" && h.RequireConnectProtocolHeader { failed = errorf(CodeInvalidArgument, "missing required header: set %s to %q", connectHeaderProtocolVersion, connectProtocolVersion) } else if version != "" && version != connectProtocolVersion { failed = errorf(CodeInvalidArgument, "%s must be %q: got %q", connectHeaderProtocolVersion, connectProtocolVersion, version) } } var requestBody io.ReadCloser var contentType, codecName string if request.Method == http.MethodGet { if failed == nil && !query.Has(connectUnaryEncodingQueryParameter) { failed = errorf(CodeInvalidArgument, "missing %s parameter", connectUnaryEncodingQueryParameter) } else if failed == nil && !query.Has(connectUnaryMessageQueryParameter) { failed = errorf(CodeInvalidArgument, "missing %s parameter", connectUnaryMessageQueryParameter) } msg := query.Get(connectUnaryMessageQueryParameter) msgReader := queryValueReader(msg, query.Get(connectUnaryBase64QueryParameter) == "1") requestBody = io.NopCloser(msgReader) codecName = query.Get(connectUnaryEncodingQueryParameter) contentType = connectContentTypeFromCodecName( h.Spec.StreamType, codecName, ) } else { requestBody = request.Body contentType = getHeaderCanonical(request.Header, headerContentType) codecName = connectCodecFromContentType( h.Spec.StreamType, contentType, ) } codec := h.Codecs.Get(codecName) // The codec can be nil in the GET request case; that's okay: when failed // is non-nil, codec is never used. if failed == nil && codec == nil { failed = errorf(CodeInvalidArgument, "invalid message encoding: %q", codecName) } // Write any remaining headers here: // (1) any writes to the stream will implicitly send the headers, so we // should get all of gRPC's required response headers ready. // (2) interceptors should be able to see these headers. // // Since we know that these header keys are already in canonical form, we can // skip the normalization in Header.Set. header := responseWriter.Header() header[headerContentType] = []string{contentType} acceptCompressionHeader := connectUnaryHeaderAcceptCompression if h.Spec.StreamType != StreamTypeUnary { acceptCompressionHeader = connectStreamingHeaderAcceptCompression // We only write the request encoding header here for streaming calls, // since the streaming envelope lets us choose whether to compress each // message individually. For unary, we won't know whether we're compressing // the request until we see how large the payload is. if responseCompression != compressionIdentity { header[connectStreamingHeaderCompression] = []string{responseCompression} } } header[acceptCompressionHeader] = []string{h.CompressionPools.CommaSeparatedNames()} var conn handlerConnCloser peer := Peer{ Addr: request.RemoteAddr, Protocol: ProtocolConnect, Query: query, } if h.Spec.StreamType == StreamTypeUnary { conn = &connectUnaryHandlerConn{ spec: h.Spec, peer: peer, request: request, responseWriter: responseWriter, marshaler: connectUnaryMarshaler{ sender: writeSender{writer: responseWriter}, codec: codec, compressMinBytes: h.CompressMinBytes, compressionName: responseCompression, compressionPool: h.CompressionPools.Get(responseCompression), bufferPool: h.BufferPool, header: responseWriter.Header(), sendMaxBytes: h.SendMaxBytes, }, unmarshaler: connectUnaryUnmarshaler{ reader: requestBody, codec: codec, compressionPool: h.CompressionPools.Get(requestCompression), bufferPool: h.BufferPool, readMaxBytes: h.ReadMaxBytes, }, responseTrailer: make(http.Header), } } else { conn = &connectStreamingHandlerConn{ spec: h.Spec, peer: peer, request: request, responseWriter: responseWriter, marshaler: connectStreamingMarshaler{ envelopeWriter: envelopeWriter{ sender: writeSender{responseWriter}, codec: codec, compressMinBytes: h.CompressMinBytes, compressionPool: h.CompressionPools.Get(responseCompression), bufferPool: h.BufferPool, sendMaxBytes: h.SendMaxBytes, }, }, unmarshaler: connectStreamingUnmarshaler{ envelopeReader: envelopeReader{ reader: requestBody, codec: codec, compressionPool: h.CompressionPools.Get(requestCompression), bufferPool: h.BufferPool, readMaxBytes: h.ReadMaxBytes, }, }, responseTrailer: make(http.Header), } } conn = wrapHandlerConnWithCodedErrors(conn) if failed != nil { // Negotiation failed, so we can't establish a stream. _ = conn.Close(failed) return nil, false } return conn, true } type connectClient struct { protocolClientParams peer Peer } func (c *connectClient) Peer() Peer { return c.peer } func (c *connectClient) WriteRequestHeader(streamType StreamType, header http.Header) { // We know these header keys are in canonical form, so we can bypass all the // checks in Header.Set. if getHeaderCanonical(header, headerUserAgent) == "" { header[headerUserAgent] = []string{defaultConnectUserAgent} } header[connectHeaderProtocolVersion] = []string{connectProtocolVersion} header[headerContentType] = []string{ connectContentTypeFromCodecName(streamType, c.Codec.Name()), } acceptCompressionHeader := connectUnaryHeaderAcceptCompression if streamType != StreamTypeUnary { // If we don't set Accept-Encoding, by default http.Client will ask the // server to compress the whole stream. Since we're already compressing // each message, this is a waste. header[connectUnaryHeaderAcceptCompression] = []string{compressionIdentity} acceptCompressionHeader = connectStreamingHeaderAcceptCompression // We only write the request encoding header here for streaming calls, // since the streaming envelope lets us choose whether to compress each // message individually. For unary, we won't know whether we're compressing // the request until we see how large the payload is. if c.CompressionName != "" && c.CompressionName != compressionIdentity { header[connectStreamingHeaderCompression] = []string{c.CompressionName} } } if acceptCompression := c.CompressionPools.CommaSeparatedNames(); acceptCompression != "" { header[acceptCompressionHeader] = []string{acceptCompression} } } func (c *connectClient) NewConn( ctx context.Context, spec Spec, header http.Header, ) streamingClientConn { if deadline, ok := ctx.Deadline(); ok { millis := int64(time.Until(deadline) / time.Millisecond) if millis > 0 { encoded := strconv.FormatInt(millis, 10 /* base */) if len(encoded) <= 10 { header[connectHeaderTimeout] = []string{encoded} } // else effectively unbounded } } duplexCall := newDuplexHTTPCall(ctx, c.HTTPClient, c.URL, spec, header) var conn streamingClientConn if spec.StreamType == StreamTypeUnary { unaryConn := &connectUnaryClientConn{ spec: spec, peer: c.Peer(), duplexCall: duplexCall, compressionPools: c.CompressionPools, bufferPool: c.BufferPool, marshaler: connectUnaryRequestMarshaler{ connectUnaryMarshaler: connectUnaryMarshaler{ sender: duplexCall, codec: c.Codec, compressMinBytes: c.CompressMinBytes, compressionName: c.CompressionName, compressionPool: c.CompressionPools.Get(c.CompressionName), bufferPool: c.BufferPool, header: duplexCall.Header(), sendMaxBytes: c.SendMaxBytes, }, }, unmarshaler: connectUnaryUnmarshaler{ reader: duplexCall, codec: c.Codec, bufferPool: c.BufferPool, readMaxBytes: c.ReadMaxBytes, }, responseHeader: make(http.Header), responseTrailer: make(http.Header), } if spec.IdempotencyLevel == IdempotencyNoSideEffects { unaryConn.marshaler.enableGet = c.EnableGet unaryConn.marshaler.getURLMaxBytes = c.GetURLMaxBytes unaryConn.marshaler.getUseFallback = c.GetUseFallback unaryConn.marshaler.duplexCall = duplexCall if stableCodec, ok := c.Codec.(stableCodec); ok { unaryConn.marshaler.stableCodec = stableCodec } } conn = unaryConn duplexCall.SetValidateResponse(unaryConn.validateResponse) } else { streamingConn := &connectStreamingClientConn{ spec: spec, peer: c.Peer(), duplexCall: duplexCall, compressionPools: c.CompressionPools, bufferPool: c.BufferPool, codec: c.Codec, marshaler: connectStreamingMarshaler{ envelopeWriter: envelopeWriter{ sender: duplexCall, codec: c.Codec, compressMinBytes: c.CompressMinBytes, compressionPool: c.CompressionPools.Get(c.CompressionName), bufferPool: c.BufferPool, sendMaxBytes: c.SendMaxBytes, }, }, unmarshaler: connectStreamingUnmarshaler{ envelopeReader: envelopeReader{ reader: duplexCall, codec: c.Codec, bufferPool: c.BufferPool, readMaxBytes: c.ReadMaxBytes, }, }, responseHeader: make(http.Header), responseTrailer: make(http.Header), } conn = streamingConn duplexCall.SetValidateResponse(streamingConn.validateResponse) } return wrapClientConnWithCodedErrors(conn) } type connectUnaryClientConn struct { spec Spec peer Peer duplexCall *duplexHTTPCall compressionPools readOnlyCompressionPools bufferPool *bufferPool marshaler connectUnaryRequestMarshaler unmarshaler connectUnaryUnmarshaler responseHeader http.Header responseTrailer http.Header } func (cc *connectUnaryClientConn) Spec() Spec { return cc.spec } func (cc *connectUnaryClientConn) Peer() Peer { return cc.peer } func (cc *connectUnaryClientConn) Send(msg any) error { if err := cc.marshaler.Marshal(msg); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (cc *connectUnaryClientConn) RequestHeader() http.Header { return cc.duplexCall.Header() } func (cc *connectUnaryClientConn) CloseRequest() error { return cc.duplexCall.CloseWrite() } func (cc *connectUnaryClientConn) Receive(msg any) error { if err := cc.duplexCall.BlockUntilResponseReady(); err != nil { return err } if err := cc.unmarshaler.Unmarshal(msg); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (cc *connectUnaryClientConn) ResponseHeader() http.Header { _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseHeader } func (cc *connectUnaryClientConn) ResponseTrailer() http.Header { _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseTrailer } func (cc *connectUnaryClientConn) CloseResponse() error { return cc.duplexCall.CloseRead() } func (cc *connectUnaryClientConn) onRequestSend(fn func(*http.Request)) { cc.duplexCall.onRequestSend = fn } func (cc *connectUnaryClientConn) validateResponse(response *http.Response) *Error { for k, v := range response.Header { if !strings.HasPrefix(k, connectUnaryTrailerPrefix) { cc.responseHeader[k] = v continue } cc.responseTrailer[strings.TrimPrefix(k, connectUnaryTrailerPrefix)] = v } compression := getHeaderCanonical(response.Header, connectUnaryHeaderCompression) if compression != "" && compression != compressionIdentity && !cc.compressionPools.Contains(compression) { return errorf( CodeInternal, "unknown encoding %q: accepted encodings are %v", compression, cc.compressionPools.CommaSeparatedNames(), ) } if response.StatusCode == http.StatusNotModified && cc.Spec().IdempotencyLevel == IdempotencyNoSideEffects { serverErr := NewWireError(CodeUnknown, errNotModifiedClient) // RFC 9110 doesn't allow trailers on 304s, so we only need to include headers. serverErr.meta = cc.responseHeader.Clone() return serverErr } else if response.StatusCode != http.StatusOK { unmarshaler := connectUnaryUnmarshaler{ reader: response.Body, compressionPool: cc.compressionPools.Get(compression), bufferPool: cc.bufferPool, } var wireErr connectWireError if err := unmarshaler.UnmarshalFunc(&wireErr, json.Unmarshal); err != nil { return NewError( connectHTTPToCode(response.StatusCode), errors.New(response.Status), ) } serverErr := wireErr.asError() if serverErr == nil { return nil } serverErr.meta = cc.responseHeader.Clone() mergeHeaders(serverErr.meta, cc.responseTrailer) return serverErr } cc.unmarshaler.compressionPool = cc.compressionPools.Get(compression) return nil } type connectStreamingClientConn struct { spec Spec peer Peer duplexCall *duplexHTTPCall compressionPools readOnlyCompressionPools bufferPool *bufferPool codec Codec marshaler connectStreamingMarshaler unmarshaler connectStreamingUnmarshaler responseHeader http.Header responseTrailer http.Header } func (cc *connectStreamingClientConn) Spec() Spec { return cc.spec } func (cc *connectStreamingClientConn) Peer() Peer { return cc.peer } func (cc *connectStreamingClientConn) Send(msg any) error { if err := cc.marshaler.Marshal(msg); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (cc *connectStreamingClientConn) RequestHeader() http.Header { return cc.duplexCall.Header() } func (cc *connectStreamingClientConn) CloseRequest() error { return cc.duplexCall.CloseWrite() } func (cc *connectStreamingClientConn) Receive(msg any) error { if err := cc.duplexCall.BlockUntilResponseReady(); err != nil { return err } err := cc.unmarshaler.Unmarshal(msg) if err == nil { return nil } // See if the server sent an explicit error in the end-of-stream message. mergeHeaders(cc.responseTrailer, cc.unmarshaler.Trailer()) if serverErr := cc.unmarshaler.EndStreamError(); serverErr != nil { // This is expected from a protocol perspective, but receiving an // end-of-stream message means that we're _not_ getting a regular message. // For users to realize that the stream has ended, Receive must return an // error. serverErr.meta = cc.responseHeader.Clone() mergeHeaders(serverErr.meta, cc.responseTrailer) _ = cc.duplexCall.CloseWrite() return serverErr } // If the error is EOF but not from a last message, we want to return // io.ErrUnexpectedEOF instead. if errors.Is(err, io.EOF) && !errors.Is(err, errSpecialEnvelope) { err = errorf(CodeInternal, "protocol error: %w", io.ErrUnexpectedEOF) } // There's no error in the trailers, so this was probably an error // converting the bytes to a message, an error reading from the network, or // just an EOF. We're going to return it to the user, but we also want to // close the writer so Send errors out. _ = cc.duplexCall.CloseWrite() return err } func (cc *connectStreamingClientConn) ResponseHeader() http.Header { _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseHeader } func (cc *connectStreamingClientConn) ResponseTrailer() http.Header { _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseTrailer } func (cc *connectStreamingClientConn) CloseResponse() error { return cc.duplexCall.CloseRead() } func (cc *connectStreamingClientConn) onRequestSend(fn func(*http.Request)) { cc.duplexCall.onRequestSend = fn } func (cc *connectStreamingClientConn) validateResponse(response *http.Response) *Error { if response.StatusCode != http.StatusOK { return errorf(connectHTTPToCode(response.StatusCode), "HTTP status %v", response.Status) } compression := getHeaderCanonical(response.Header, connectStreamingHeaderCompression) if compression != "" && compression != compressionIdentity && !cc.compressionPools.Contains(compression) { return errorf( CodeInternal, "unknown encoding %q: accepted encodings are %v", compression, cc.compressionPools.CommaSeparatedNames(), ) } cc.unmarshaler.compressionPool = cc.compressionPools.Get(compression) mergeHeaders(cc.responseHeader, response.Header) return nil } type connectUnaryHandlerConn struct { spec Spec peer Peer request *http.Request responseWriter http.ResponseWriter marshaler connectUnaryMarshaler unmarshaler connectUnaryUnmarshaler responseTrailer http.Header wroteBody bool } func (hc *connectUnaryHandlerConn) Spec() Spec { return hc.spec } func (hc *connectUnaryHandlerConn) Peer() Peer { return hc.peer } func (hc *connectUnaryHandlerConn) Receive(msg any) error { if err := hc.unmarshaler.Unmarshal(msg); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (hc *connectUnaryHandlerConn) RequestHeader() http.Header { return hc.request.Header } func (hc *connectUnaryHandlerConn) Send(msg any) error { hc.wroteBody = true hc.writeResponseHeader(nil /* error */) if err := hc.marshaler.Marshal(msg); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (hc *connectUnaryHandlerConn) ResponseHeader() http.Header { return hc.responseWriter.Header() } func (hc *connectUnaryHandlerConn) ResponseTrailer() http.Header { return hc.responseTrailer } func (hc *connectUnaryHandlerConn) Close(err error) error { if !hc.wroteBody { hc.writeResponseHeader(err) // If the handler received a GET request and the resource hasn't changed, // return a 304. if len(hc.peer.Query) > 0 && IsNotModifiedError(err) { hc.responseWriter.WriteHeader(http.StatusNotModified) return hc.request.Body.Close() } } if err == nil { return hc.request.Body.Close() } // In unary Connect, errors always use application/json. setHeaderCanonical(hc.responseWriter.Header(), headerContentType, connectUnaryContentTypeJSON) hc.responseWriter.WriteHeader(connectCodeToHTTP(CodeOf(err))) data, marshalErr := json.Marshal(newConnectWireError(err)) if marshalErr != nil { _ = hc.request.Body.Close() return errorf(CodeInternal, "marshal error: %w", err) } if _, writeErr := hc.responseWriter.Write(data); writeErr != nil { _ = hc.request.Body.Close() return writeErr } return hc.request.Body.Close() } func (hc *connectUnaryHandlerConn) getHTTPMethod() string { return hc.request.Method } func (hc *connectUnaryHandlerConn) writeResponseHeader(err error) { header := hc.responseWriter.Header() if hc.request.Method == http.MethodGet { // The response content varies depending on the compression that the client // requested (if any). GETs are potentially cacheable, so we should ensure // that the Vary header includes at least Accept-Encoding (and not overwrite any values already set). header[headerVary] = append(header[headerVary], connectUnaryHeaderAcceptCompression) } if err != nil { if connectErr, ok := asError(err); ok { mergeHeaders(header, connectErr.meta) } } for k, v := range hc.responseTrailer { header[connectUnaryTrailerPrefix+k] = v } } type connectStreamingHandlerConn struct { spec Spec peer Peer request *http.Request responseWriter http.ResponseWriter marshaler connectStreamingMarshaler unmarshaler connectStreamingUnmarshaler responseTrailer http.Header } func (hc *connectStreamingHandlerConn) Spec() Spec { return hc.spec } func (hc *connectStreamingHandlerConn) Peer() Peer { return hc.peer } func (hc *connectStreamingHandlerConn) Receive(msg any) error { if err := hc.unmarshaler.Unmarshal(msg); err != nil { // Clients may not send end-of-stream metadata, so we don't need to handle // errSpecialEnvelope. return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (hc *connectStreamingHandlerConn) RequestHeader() http.Header { return hc.request.Header } func (hc *connectStreamingHandlerConn) Send(msg any) error { defer flushResponseWriter(hc.responseWriter) if err := hc.marshaler.Marshal(msg); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (hc *connectStreamingHandlerConn) ResponseHeader() http.Header { return hc.responseWriter.Header() } func (hc *connectStreamingHandlerConn) ResponseTrailer() http.Header { return hc.responseTrailer } func (hc *connectStreamingHandlerConn) Close(err error) error { defer flushResponseWriter(hc.responseWriter) if err := hc.marshaler.MarshalEndStream(err, hc.responseTrailer); err != nil { _ = hc.request.Body.Close() return err } // We don't want to copy unread portions of the body to /dev/null here: if // the client hasn't closed the request body, we'll block until the server // timeout kicks in. This could happen because the client is malicious, but // a well-intentioned client may just not expect the server to be returning // an error for a streaming RPC. Better to accept that we can't always reuse // TCP connections. if err := hc.request.Body.Close(); err != nil { if connectErr, ok := asError(err); ok { return connectErr } return NewError(CodeUnknown, err) } return nil // must be a literal nil: nil *Error is a non-nil error } type connectStreamingMarshaler struct { envelopeWriter } func (m *connectStreamingMarshaler) MarshalEndStream(err error, trailer http.Header) *Error { end := &connectEndStreamMessage{Trailer: trailer} if err != nil { end.Error = newConnectWireError(err) if connectErr, ok := asError(err); ok { mergeHeaders(end.Trailer, connectErr.meta) } } data, marshalErr := json.Marshal(end) if marshalErr != nil { return errorf(CodeInternal, "marshal end stream: %w", marshalErr) } raw := bytes.NewBuffer(data) defer m.envelopeWriter.bufferPool.Put(raw) return m.Write(&envelope{ Data: raw, Flags: connectFlagEnvelopeEndStream, }) } type connectStreamingUnmarshaler struct { envelopeReader endStreamErr *Error trailer http.Header } func (u *connectStreamingUnmarshaler) Unmarshal(message any) *Error { err := u.envelopeReader.Unmarshal(message) if err == nil { return nil } if !errors.Is(err, errSpecialEnvelope) { return err } env := u.envelopeReader.last if !env.IsSet(connectFlagEnvelopeEndStream) { return errorf(CodeInternal, "protocol error: invalid envelope flags %d", env.Flags) } var end connectEndStreamMessage if err := json.Unmarshal(env.Data.Bytes(), &end); err != nil { return errorf(CodeInternal, "unmarshal end stream message: %w", err) } for name, value := range end.Trailer { canonical := http.CanonicalHeaderKey(name) if name != canonical { delHeaderCanonical(end.Trailer, name) end.Trailer[canonical] = append(end.Trailer[canonical], value...) } } u.trailer = end.Trailer u.endStreamErr = end.Error.asError() return errSpecialEnvelope } func (u *connectStreamingUnmarshaler) Trailer() http.Header { return u.trailer } func (u *connectStreamingUnmarshaler) EndStreamError() *Error { return u.endStreamErr } type connectUnaryMarshaler struct { sender messageSender codec Codec compressMinBytes int compressionName string compressionPool *compressionPool bufferPool *bufferPool header http.Header sendMaxBytes int } func (m *connectUnaryMarshaler) Marshal(message any) *Error { if message == nil { return m.write(nil) } var data []byte var err error if appender, ok := m.codec.(marshalAppender); ok { data, err = appender.MarshalAppend(m.bufferPool.Get().Bytes(), message) } else { // Can't avoid allocating the slice, but we'll reuse it. data, err = m.codec.Marshal(message) } if err != nil { return errorf(CodeInternal, "marshal message: %w", err) } uncompressed := bytes.NewBuffer(data) defer m.bufferPool.Put(uncompressed) if len(data) < m.compressMinBytes || m.compressionPool == nil { if m.sendMaxBytes > 0 && len(data) > m.sendMaxBytes { return NewError(CodeResourceExhausted, fmt.Errorf("message size %d exceeds sendMaxBytes %d", len(data), m.sendMaxBytes)) } return m.write(data) } compressed := m.bufferPool.Get() defer m.bufferPool.Put(compressed) if err := m.compressionPool.Compress(compressed, uncompressed); err != nil { return err } if m.sendMaxBytes > 0 && compressed.Len() > m.sendMaxBytes { return NewError(CodeResourceExhausted, fmt.Errorf("compressed message size %d exceeds sendMaxBytes %d", compressed.Len(), m.sendMaxBytes)) } setHeaderCanonical(m.header, connectUnaryHeaderCompression, m.compressionName) return m.write(compressed.Bytes()) } func (m *connectUnaryMarshaler) write(data []byte) *Error { payload := bytes.NewReader(data) if _, err := m.sender.Send(payload); err != nil { err = wrapIfContextError(err) if connectErr, ok := asError(err); ok { return connectErr } return errorf(CodeUnknown, "write message: %w", err) } return nil } type connectUnaryRequestMarshaler struct { connectUnaryMarshaler enableGet bool getURLMaxBytes int getUseFallback bool stableCodec stableCodec duplexCall *duplexHTTPCall } func (m *connectUnaryRequestMarshaler) Marshal(message any) *Error { if m.enableGet { if m.stableCodec == nil && !m.getUseFallback { return errorf(CodeInternal, "codec %s doesn't support stable marshal; can't use get", m.codec.Name()) } if m.stableCodec != nil { return m.marshalWithGet(message) } } return m.connectUnaryMarshaler.Marshal(message) } func (m *connectUnaryRequestMarshaler) marshalWithGet(message any) *Error { // TODO(jchadwick-buf): This function is mostly a superset of // connectUnaryMarshaler.Marshal. This should be reconciled at some point. var data []byte var err error if message != nil { data, err = m.stableCodec.MarshalStable(message) if err != nil { return errorf(CodeInternal, "marshal message stable: %w", err) } } isTooBig := m.sendMaxBytes > 0 && len(data) > m.sendMaxBytes if isTooBig && m.compressionPool == nil { return NewError(CodeResourceExhausted, fmt.Errorf( "message size %d exceeds sendMaxBytes %d: enabling request compression may help", len(data), m.sendMaxBytes, )) } if !isTooBig { url := m.buildGetURL(data, false /* compressed */) if m.getURLMaxBytes <= 0 || len(url.String()) < m.getURLMaxBytes { return m.writeWithGet(url) } if m.compressionPool == nil { if m.getUseFallback { return m.write(data) } return NewError(CodeResourceExhausted, fmt.Errorf( "url size %d exceeds getURLMaxBytes %d: enabling request compression may help", len(url.String()), m.getURLMaxBytes, )) } } // Compress message to try to make it fit in the URL. uncompressed := bytes.NewBuffer(data) defer m.bufferPool.Put(uncompressed) compressed := m.bufferPool.Get() defer m.bufferPool.Put(compressed) if err := m.compressionPool.Compress(compressed, uncompressed); err != nil { return err } if m.sendMaxBytes > 0 && compressed.Len() > m.sendMaxBytes { return NewError(CodeResourceExhausted, fmt.Errorf("compressed message size %d exceeds sendMaxBytes %d", compressed.Len(), m.sendMaxBytes)) } url := m.buildGetURL(compressed.Bytes(), true /* compressed */) if m.getURLMaxBytes <= 0 || len(url.String()) < m.getURLMaxBytes { return m.writeWithGet(url) } if m.getUseFallback { setHeaderCanonical(m.header, connectUnaryHeaderCompression, m.compressionName) return m.write(compressed.Bytes()) } return NewError(CodeResourceExhausted, fmt.Errorf("compressed url size %d exceeds getURLMaxBytes %d", len(url.String()), m.getURLMaxBytes)) } func (m *connectUnaryRequestMarshaler) buildGetURL(data []byte, compressed bool) *url.URL { url := *m.duplexCall.URL() query := url.Query() query.Set(connectUnaryConnectQueryParameter, connectUnaryConnectQueryValue) query.Set(connectUnaryEncodingQueryParameter, m.codec.Name()) if m.stableCodec.IsBinary() || compressed { query.Set(connectUnaryMessageQueryParameter, encodeBinaryQueryValue(data)) query.Set(connectUnaryBase64QueryParameter, "1") } else { query.Set(connectUnaryMessageQueryParameter, string(data)) } if compressed { query.Set(connectUnaryCompressionQueryParameter, m.compressionName) } url.RawQuery = query.Encode() return &url } func (m *connectUnaryRequestMarshaler) writeWithGet(url *url.URL) *Error { delHeaderCanonical(m.header, connectHeaderProtocolVersion) delHeaderCanonical(m.header, headerContentType) delHeaderCanonical(m.header, headerContentEncoding) delHeaderCanonical(m.header, headerContentLength) m.duplexCall.SetMethod(http.MethodGet) *m.duplexCall.URL() = *url return nil } type connectUnaryUnmarshaler struct { reader io.Reader codec Codec compressionPool *compressionPool bufferPool *bufferPool alreadyRead bool readMaxBytes int } func (u *connectUnaryUnmarshaler) Unmarshal(message any) *Error { return u.UnmarshalFunc(message, u.codec.Unmarshal) } func (u *connectUnaryUnmarshaler) UnmarshalFunc(message any, unmarshal func([]byte, any) error) *Error { if u.alreadyRead { return NewError(CodeInternal, io.EOF) } u.alreadyRead = true data := u.bufferPool.Get() defer u.bufferPool.Put(data) reader := u.reader if u.readMaxBytes > 0 && int64(u.readMaxBytes) < math.MaxInt64 { reader = io.LimitReader(u.reader, int64(u.readMaxBytes)+1) } // ReadFrom ignores io.EOF, so any error here is real. bytesRead, err := data.ReadFrom(reader) if err != nil { err = wrapIfContextError(err) if connectErr, ok := asError(err); ok { return connectErr } if readMaxBytesErr := asMaxBytesError(err, "read first %d bytes of message", bytesRead); readMaxBytesErr != nil { return readMaxBytesErr } return errorf(CodeUnknown, "read message: %w", err) } if u.readMaxBytes > 0 && bytesRead > int64(u.readMaxBytes) { // Attempt to read to end in order to allow connection re-use discardedBytes, err := io.Copy(io.Discard, u.reader) if err != nil { return errorf(CodeResourceExhausted, "message is larger than configured max %d - unable to determine message size: %w", u.readMaxBytes, err) } return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", bytesRead+discardedBytes, u.readMaxBytes) } if data.Len() > 0 && u.compressionPool != nil { decompressed := u.bufferPool.Get() defer u.bufferPool.Put(decompressed) if err := u.compressionPool.Decompress(decompressed, data, int64(u.readMaxBytes)); err != nil { return err } data = decompressed } if err := unmarshal(data.Bytes(), message); err != nil { return errorf(CodeInvalidArgument, "unmarshal message: %w", err) } return nil } type connectWireDetail ErrorDetail func (d *connectWireDetail) MarshalJSON() ([]byte, error) { if d.wireJSON != "" { // If we unmarshaled this detail from JSON, return the original data. This // lets proxies w/o protobuf descriptors preserve human-readable details. return []byte(d.wireJSON), nil } wire := struct { Type string `json:"type"` Value string `json:"value"` Debug json.RawMessage `json:"debug,omitempty"` }{ Type: typeNameFromURL(d.pb.GetTypeUrl()), Value: base64.RawStdEncoding.EncodeToString(d.pb.GetValue()), } // Try to produce debug info, but expect failure when we don't have // descriptors. var codec protoJSONCodec debug, err := codec.Marshal(d.pb) if err == nil && len(debug) > 2 { // don't bother sending `{}` wire.Debug = json.RawMessage(debug) } return json.Marshal(wire) } func (d *connectWireDetail) UnmarshalJSON(data []byte) error { var wire struct { Type string `json:"type"` Value string `json:"value"` } if err := json.Unmarshal(data, &wire); err != nil { return err } if !strings.Contains(wire.Type, "/") { wire.Type = defaultAnyResolverPrefix + wire.Type } decoded, err := DecodeBinaryHeader(wire.Value) if err != nil { return fmt.Errorf("decode base64: %w", err) } *d = connectWireDetail{ pb: &anypb.Any{ TypeUrl: wire.Type, Value: decoded, }, wireJSON: string(data), } return nil } type connectWireError struct { Code Code `json:"code"` Message string `json:"message,omitempty"` Details []*connectWireDetail `json:"details,omitempty"` } func newConnectWireError(err error) *connectWireError { wire := &connectWireError{ Code: CodeUnknown, Message: err.Error(), } if connectErr, ok := asError(err); ok { wire.Code = connectErr.Code() wire.Message = connectErr.Message() if len(connectErr.details) > 0 { wire.Details = make([]*connectWireDetail, len(connectErr.details)) for i, detail := range connectErr.details { wire.Details[i] = (*connectWireDetail)(detail) } } } return wire } func (e *connectWireError) asError() *Error { if e == nil { return nil } if e.Code < minCode || e.Code > maxCode { e.Code = CodeUnknown } err := NewWireError(e.Code, errors.New(e.Message)) if len(e.Details) > 0 { err.details = make([]*ErrorDetail, len(e.Details)) for i, detail := range e.Details { err.details[i] = (*ErrorDetail)(detail) } } return err } type connectEndStreamMessage struct { Error *connectWireError `json:"error,omitempty"` Trailer http.Header `json:"metadata,omitempty"` } func connectCodeToHTTP(code Code) int { // Return literals rather than named constants from the HTTP package to make // it easier to compare this function to the Connect specification. switch code { case CodeCanceled: return 408 case CodeUnknown: return 500 case CodeInvalidArgument: return 400 case CodeDeadlineExceeded: return 408 case CodeNotFound: return 404 case CodeAlreadyExists: return 409 case CodePermissionDenied: return 403 case CodeResourceExhausted: return 429 case CodeFailedPrecondition: return 412 case CodeAborted: return 409 case CodeOutOfRange: return 400 case CodeUnimplemented: return 404 case CodeInternal: return 500 case CodeUnavailable: return 503 case CodeDataLoss: return 500 case CodeUnauthenticated: return 401 default: return 500 // same as CodeUnknown } } func connectHTTPToCode(httpCode int) Code { // As above, literals are easier to compare to the specificaton (vs named // constants). switch httpCode { case 400: return CodeInvalidArgument case 401: return CodeUnauthenticated case 403: return CodePermissionDenied case 404: return CodeUnimplemented case 408: return CodeDeadlineExceeded case 412: return CodeFailedPrecondition case 413: return CodeResourceExhausted case 429: return CodeUnavailable case 431: return CodeResourceExhausted case 502, 503, 504: return CodeUnavailable default: return CodeUnknown } } func connectCodecFromContentType(streamType StreamType, contentType string) string { if streamType == StreamTypeUnary { return strings.TrimPrefix(contentType, connectUnaryContentTypePrefix) } return strings.TrimPrefix(contentType, connectStreamingContentTypePrefix) } func connectContentTypeFromCodecName(streamType StreamType, name string) string { if streamType == StreamTypeUnary { return connectUnaryContentTypePrefix + name } return connectStreamingContentTypePrefix + name } // encodeBinaryQueryValue URL-safe base64-encodes data, without padding. func encodeBinaryQueryValue(data []byte) string { return base64.RawURLEncoding.EncodeToString(data) } // binaryQueryValueReader creates a reader that can read either padded or // unpadded URL-safe base64 from a string. func binaryQueryValueReader(data string) io.Reader { stringReader := strings.NewReader(data) if len(data)%4 != 0 { // Data definitely isn't padded. return base64.NewDecoder(base64.RawURLEncoding, stringReader) } // Data is padded, or no padding was necessary. return base64.NewDecoder(base64.URLEncoding, stringReader) } // queryValueReader creates a reader for a string that may be URL-safe base64 // encoded. func queryValueReader(data string, base64Encoded bool) io.Reader { if base64Encoded { return binaryQueryValueReader(data) } return strings.NewReader(data) } connect-go-1.13.0/protocol_connect_test.go000066400000000000000000000061071453471351600206050ustar00rootroot00000000000000// Copyright 2021-2023 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" "net/http" "strings" "testing" "time" "connectrpc.com/connect/internal/assert" "google.golang.org/protobuf/types/known/durationpb" ) func TestConnectErrorDetailMarshaling(t *testing.T) { t.Parallel() detail, err := NewErrorDetail(durationpb.New(time.Second)) assert.Nil(t, err) data, err := json.Marshal((*connectWireDetail)(detail)) assert.Nil(t, err) t.Logf("marshaled error detail: %s", string(data)) var unmarshaled connectWireDetail assert.Nil(t, json.Unmarshal(data, &unmarshaled)) assert.Equal(t, unmarshaled.wireJSON, string(data)) assert.Equal(t, unmarshaled.pb, detail.pb) } func TestConnectErrorDetailMarshalingNoDescriptor(t *testing.T) { t.Parallel() raw := `{"type":"acme.user.v1.User","value":"DEADBF",` + `"debug":{"@type":"acme.user.v1.User","email":"someone@connectrpc.com"}}` var detail connectWireDetail assert.Nil(t, json.Unmarshal([]byte(raw), &detail)) assert.Equal(t, detail.pb.GetTypeUrl(), defaultAnyResolverPrefix+"acme.user.v1.User") _, err := (*ErrorDetail)(&detail).Value() assert.NotNil(t, err) assert.True(t, strings.HasSuffix(err.Error(), "not found")) encoded, err := json.Marshal(&detail) assert.Nil(t, err) assert.Equal(t, string(encoded), raw) } func TestConnectEndOfResponseCanonicalTrailers(t *testing.T) { t.Parallel() buffer := bytes.Buffer{} bufferPool := newBufferPool() endStreamMessage := connectEndStreamMessage{Trailer: make(http.Header)} endStreamMessage.Trailer["not-canonical-header"] = []string{"a"} endStreamMessage.Trailer["mixed-Canonical"] = []string{"b"} endStreamMessage.Trailer["Mixed-Canonical"] = []string{"b"} endStreamMessage.Trailer["Canonical-Header"] = []string{"c"} endStreamData, err := json.Marshal(endStreamMessage) assert.Nil(t, err) writer := envelopeWriter{ sender: writeSender{writer: &buffer}, bufferPool: bufferPool, } err = writer.Write(&envelope{ Flags: connectFlagEnvelopeEndStream, Data: bytes.NewBuffer(endStreamData), }) assert.Nil(t, err) unmarshaler := connectStreamingUnmarshaler{ envelopeReader: envelopeReader{ reader: &buffer, bufferPool: bufferPool, }, } err = unmarshaler.Unmarshal(nil) // parameter won't be used assert.ErrorIs(t, err, errSpecialEnvelope) assert.Equal(t, unmarshaler.Trailer().Values("Not-Canonical-Header"), []string{"a"}) assert.Equal(t, unmarshaler.Trailer().Values("Mixed-Canonical"), []string{"b", "b"}) assert.Equal(t, unmarshaler.Trailer().Values("Canonical-Header"), []string{"c"}) } connect-go-1.13.0/protocol_grpc.go000066400000000000000000000764321453471351600170600ustar00rootroot00000000000000// Copyright 2021-2023 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 ( "bufio" "context" "errors" "fmt" "io" "math" "net/http" "net/textproto" "runtime" "strconv" "strings" "time" statusv1 "connectrpc.com/connect/internal/gen/connectext/grpc/status/v1" ) const ( grpcHeaderCompression = "Grpc-Encoding" grpcHeaderAcceptCompression = "Grpc-Accept-Encoding" grpcHeaderTimeout = "Grpc-Timeout" grpcHeaderStatus = "Grpc-Status" grpcHeaderMessage = "Grpc-Message" grpcHeaderDetails = "Grpc-Status-Details-Bin" grpcFlagEnvelopeTrailer = 0b10000000 grpcContentTypeDefault = "application/grpc" grpcWebContentTypeDefault = "application/grpc-web" grpcContentTypePrefix = grpcContentTypeDefault + "+" grpcWebContentTypePrefix = grpcWebContentTypeDefault + "+" headerXUserAgent = "X-User-Agent" upperhex = "0123456789ABCDEF" ) var ( errTrailersWithoutGRPCStatus = fmt.Errorf("protocol error: no %s trailer: %w", grpcHeaderStatus, io.ErrUnexpectedEOF) // defaultGrpcUserAgent follows // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#user-agents: // // While the protocol does not require a user-agent to function it is recommended // that clients provide a structured user-agent string that provides a basic // description of the calling library, version & platform to facilitate issue diagnosis // in heterogeneous environments. The following structure is recommended to library developers: // // User-Agent → "grpc-" Language ?("-" Variant) "/" Version ?( " (" *(AdditionalProperty ";") ")" ) defaultGrpcUserAgent = fmt.Sprintf("grpc-go-connect/%s (%s)", Version, runtime.Version()) grpcAllowedMethods = map[string]struct{}{ http.MethodPost: {}, } ) type protocolGRPC struct { web bool } // NewHandler implements protocol, so it must return an interface. func (g *protocolGRPC) NewHandler(params *protocolHandlerParams) protocolHandler { bare, prefix := grpcContentTypeDefault, grpcContentTypePrefix if g.web { bare, prefix = grpcWebContentTypeDefault, grpcWebContentTypePrefix } contentTypes := make(map[string]struct{}) for _, name := range params.Codecs.Names() { contentTypes[canonicalizeContentType(prefix+name)] = struct{}{} } if params.Codecs.Get(codecNameProto) != nil { contentTypes[bare] = struct{}{} } return &grpcHandler{ protocolHandlerParams: *params, web: g.web, accept: contentTypes, } } // NewClient implements protocol, so it must return an interface. func (g *protocolGRPC) NewClient(params *protocolClientParams) (protocolClient, error) { peer := newPeerFromURL(params.URL, ProtocolGRPC) if g.web { peer = newPeerFromURL(params.URL, ProtocolGRPCWeb) } return &grpcClient{ protocolClientParams: *params, web: g.web, peer: peer, }, nil } type grpcHandler struct { protocolHandlerParams web bool accept map[string]struct{} } func (g *grpcHandler) Methods() map[string]struct{} { return grpcAllowedMethods } func (g *grpcHandler) ContentTypes() map[string]struct{} { return g.accept } func (*grpcHandler) SetTimeout(request *http.Request) (context.Context, context.CancelFunc, error) { timeout, err := grpcParseTimeout(getHeaderCanonical(request.Header, grpcHeaderTimeout)) if err != nil && !errors.Is(err, errNoTimeout) { // Errors here indicate that the client sent an invalid timeout header, so // the error text is safe to send back. return nil, nil, NewError(CodeInvalidArgument, err) } else if err != nil { // err wraps errNoTimeout, nothing to do. return request.Context(), nil, nil //nolint:nilerr } ctx, cancel := context.WithTimeout(request.Context(), timeout) return ctx, cancel, nil } func (g *grpcHandler) CanHandlePayload(request *http.Request, contentType string) bool { _, ok := g.accept[contentType] return ok } func (g *grpcHandler) NewConn( responseWriter http.ResponseWriter, request *http.Request, ) (handlerConnCloser, bool) { // We need to parse metadata before entering the interceptor stack; we'll // send the error to the client later on. requestCompression, responseCompression, failed := negotiateCompression( g.CompressionPools, getHeaderCanonical(request.Header, grpcHeaderCompression), getHeaderCanonical(request.Header, grpcHeaderAcceptCompression), ) if failed == nil { failed = checkServerStreamsCanFlush(g.Spec, responseWriter) } // Write any remaining headers here: // (1) any writes to the stream will implicitly send the headers, so we // should get all of gRPC's required response headers ready. // (2) interceptors should be able to see these headers. // // Since we know that these header keys are already in canonical form, we can // skip the normalization in Header.Set. header := responseWriter.Header() header[headerContentType] = []string{getHeaderCanonical(request.Header, headerContentType)} header[grpcHeaderAcceptCompression] = []string{g.CompressionPools.CommaSeparatedNames()} if responseCompression != compressionIdentity { header[grpcHeaderCompression] = []string{responseCompression} } codecName := grpcCodecFromContentType(g.web, getHeaderCanonical(request.Header, headerContentType)) codec := g.Codecs.Get(codecName) // handler.go guarantees this is not nil protocolName := ProtocolGRPC if g.web { protocolName = ProtocolGRPCWeb } conn := wrapHandlerConnWithCodedErrors(&grpcHandlerConn{ spec: g.Spec, peer: Peer{ Addr: request.RemoteAddr, Protocol: protocolName, }, web: g.web, bufferPool: g.BufferPool, protobuf: g.Codecs.Protobuf(), // for errors marshaler: grpcMarshaler{ envelopeWriter: envelopeWriter{ sender: writeSender{writer: responseWriter}, compressionPool: g.CompressionPools.Get(responseCompression), codec: codec, compressMinBytes: g.CompressMinBytes, bufferPool: g.BufferPool, sendMaxBytes: g.SendMaxBytes, }, }, responseWriter: responseWriter, responseHeader: make(http.Header), responseTrailer: make(http.Header), request: request, unmarshaler: grpcUnmarshaler{ envelopeReader: envelopeReader{ reader: request.Body, codec: codec, compressionPool: g.CompressionPools.Get(requestCompression), bufferPool: g.BufferPool, readMaxBytes: g.ReadMaxBytes, }, web: g.web, }, }) if failed != nil { // Negotiation failed, so we can't establish a stream. _ = conn.Close(failed) return nil, false } return conn, true } type grpcClient struct { protocolClientParams web bool peer Peer } func (g *grpcClient) Peer() Peer { return g.peer } func (g *grpcClient) WriteRequestHeader(_ StreamType, header http.Header) { // We know these header keys are in canonical form, so we can bypass all the // checks in Header.Set. if getHeaderCanonical(header, headerUserAgent) == "" { header[headerUserAgent] = []string{defaultGrpcUserAgent} } if g.web && getHeaderCanonical(header, headerXUserAgent) == "" { // The gRPC-Web pseudo-specification seems to require X-User-Agent rather // than User-Agent for all clients, even if they're not browser-based. This // is very odd for a backend client, so we'll split the difference and set // both. header[headerXUserAgent] = []string{defaultGrpcUserAgent} } header[headerContentType] = []string{grpcContentTypeFromCodecName(g.web, g.Codec.Name())} // gRPC handles compression on a per-message basis, so we don't want to // compress the whole stream. By default, http.Client will ask the server // to gzip the stream if we don't set Accept-Encoding. header["Accept-Encoding"] = []string{compressionIdentity} if g.CompressionName != "" && g.CompressionName != compressionIdentity { header[grpcHeaderCompression] = []string{g.CompressionName} } if acceptCompression := g.CompressionPools.CommaSeparatedNames(); acceptCompression != "" { header[grpcHeaderAcceptCompression] = []string{acceptCompression} } if !g.web { // The gRPC-HTTP2 specification requires this - it flushes out proxies that // don't support HTTP trailers. header["Te"] = []string{"trailers"} } } func (g *grpcClient) NewConn( ctx context.Context, spec Spec, header http.Header, ) streamingClientConn { if deadline, ok := ctx.Deadline(); ok { encodedDeadline := grpcEncodeTimeout(time.Until(deadline)) header[grpcHeaderTimeout] = []string{encodedDeadline} } duplexCall := newDuplexHTTPCall( ctx, g.HTTPClient, g.URL, spec, header, ) conn := &grpcClientConn{ spec: spec, peer: g.Peer(), duplexCall: duplexCall, compressionPools: g.CompressionPools, bufferPool: g.BufferPool, protobuf: g.Protobuf, marshaler: grpcMarshaler{ envelopeWriter: envelopeWriter{ sender: duplexCall, compressionPool: g.CompressionPools.Get(g.CompressionName), codec: g.Codec, compressMinBytes: g.CompressMinBytes, bufferPool: g.BufferPool, sendMaxBytes: g.SendMaxBytes, }, }, unmarshaler: grpcUnmarshaler{ envelopeReader: envelopeReader{ reader: duplexCall, codec: g.Codec, bufferPool: g.BufferPool, readMaxBytes: g.ReadMaxBytes, }, }, responseHeader: make(http.Header), responseTrailer: make(http.Header), } duplexCall.SetValidateResponse(conn.validateResponse) if g.web { conn.unmarshaler.web = true conn.readTrailers = func(unmarshaler *grpcUnmarshaler, _ *duplexHTTPCall) http.Header { return unmarshaler.WebTrailer() } } else { conn.readTrailers = func(_ *grpcUnmarshaler, call *duplexHTTPCall) http.Header { // To access HTTP trailers, we need to read the body to EOF. _, _ = discard(call) return call.ResponseTrailer() } } return wrapClientConnWithCodedErrors(conn) } // grpcClientConn works for both gRPC and gRPC-Web. type grpcClientConn struct { spec Spec peer Peer duplexCall *duplexHTTPCall compressionPools readOnlyCompressionPools bufferPool *bufferPool protobuf Codec // for errors marshaler grpcMarshaler unmarshaler grpcUnmarshaler responseHeader http.Header responseTrailer http.Header readTrailers func(*grpcUnmarshaler, *duplexHTTPCall) http.Header } func (cc *grpcClientConn) Spec() Spec { return cc.spec } func (cc *grpcClientConn) Peer() Peer { return cc.peer } func (cc *grpcClientConn) Send(msg any) error { if err := cc.marshaler.Marshal(msg); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (cc *grpcClientConn) RequestHeader() http.Header { return cc.duplexCall.Header() } func (cc *grpcClientConn) CloseRequest() error { return cc.duplexCall.CloseWrite() } func (cc *grpcClientConn) Receive(msg any) error { if err := cc.duplexCall.BlockUntilResponseReady(); err != nil { return err } err := cc.unmarshaler.Unmarshal(msg) if err == nil { return nil } if getHeaderCanonical(cc.responseHeader, grpcHeaderStatus) != "" { // We got what gRPC calls a trailers-only response, which puts the trailing // metadata (including errors) into HTTP headers. validateResponse has // already extracted the error. return err } // See if the server sent an explicit error in the HTTP or gRPC-Web trailers. mergeHeaders( cc.responseTrailer, cc.readTrailers(&cc.unmarshaler, cc.duplexCall), ) serverErr := grpcErrorFromTrailer(cc.protobuf, cc.responseTrailer) if serverErr != nil && (errors.Is(err, io.EOF) || !errors.Is(serverErr, errTrailersWithoutGRPCStatus)) { // We've either: // - Cleanly read until the end of the response body and *not* received // gRPC status trailers, which is a protocol error, or // - Received an explicit error from the server. // // This is expected from a protocol perspective, but receiving trailers // means that we're _not_ getting a message. For users to realize that // the stream has ended, Receive must return an error. serverErr.meta = cc.responseHeader.Clone() mergeHeaders(serverErr.meta, cc.responseTrailer) _ = cc.duplexCall.CloseWrite() return serverErr } // This was probably an error converting the bytes to a message or an error // reading from the network. We're going to return it to the // user, but we also want to close writes so Send errors out. _ = cc.duplexCall.CloseWrite() return err } func (cc *grpcClientConn) ResponseHeader() http.Header { _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseHeader } func (cc *grpcClientConn) ResponseTrailer() http.Header { _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseTrailer } func (cc *grpcClientConn) CloseResponse() error { return cc.duplexCall.CloseRead() } func (cc *grpcClientConn) onRequestSend(fn func(*http.Request)) { cc.duplexCall.onRequestSend = fn } func (cc *grpcClientConn) validateResponse(response *http.Response) *Error { if err := grpcValidateResponse( response, cc.responseHeader, cc.responseTrailer, cc.compressionPools, cc.protobuf, ); err != nil { return err } compression := getHeaderCanonical(response.Header, grpcHeaderCompression) cc.unmarshaler.envelopeReader.compressionPool = cc.compressionPools.Get(compression) return nil } type grpcHandlerConn struct { spec Spec peer Peer web bool bufferPool *bufferPool protobuf Codec // for errors marshaler grpcMarshaler responseWriter http.ResponseWriter responseHeader http.Header responseTrailer http.Header wroteToBody bool request *http.Request unmarshaler grpcUnmarshaler } func (hc *grpcHandlerConn) Spec() Spec { return hc.spec } func (hc *grpcHandlerConn) Peer() Peer { return hc.peer } func (hc *grpcHandlerConn) Receive(msg any) error { if err := hc.unmarshaler.Unmarshal(msg); err != nil { return err // already coded } return nil // must be a literal nil: nil *Error is a non-nil error } func (hc *grpcHandlerConn) RequestHeader() http.Header { return hc.request.Header } func (hc *grpcHandlerConn) Send(msg any) error { defer flushResponseWriter(hc.responseWriter) if !hc.wroteToBody { mergeHeaders(hc.responseWriter.Header(), hc.responseHeader) hc.wroteToBody = true } if err := hc.marshaler.Marshal(msg); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (hc *grpcHandlerConn) ResponseHeader() http.Header { return hc.responseHeader } func (hc *grpcHandlerConn) ResponseTrailer() http.Header { return hc.responseTrailer } func (hc *grpcHandlerConn) Close(err error) (retErr error) { defer func() { // We don't want to copy unread portions of the body to /dev/null here: if // the client hasn't closed the request body, we'll block until the server // timeout kicks in. This could happen because the client is malicious, but // a well-intentioned client may just not expect the server to be returning // an error for a streaming RPC. Better to accept that we can't always reuse // TCP connections. closeErr := hc.request.Body.Close() if retErr == nil { retErr = closeErr } }() defer flushResponseWriter(hc.responseWriter) // If we haven't written the headers yet, do so. if !hc.wroteToBody { mergeHeaders(hc.responseWriter.Header(), hc.responseHeader) } // gRPC always sends the error's code, message, details, and metadata as // trailing metadata. The Connect protocol doesn't do this, so we don't want // to mutate the trailers map that the user sees. mergedTrailers := make( http.Header, len(hc.responseTrailer)+2, // always make space for status & message ) mergeHeaders(mergedTrailers, hc.responseTrailer) grpcErrorToTrailer(mergedTrailers, hc.protobuf, err) if hc.web && !hc.wroteToBody { // We're using gRPC-Web and we haven't yet written to the body. Since we're // not sending any response messages, the gRPC specification calls this a // "trailers-only" response. Under those circumstances, the gRPC-Web spec // says that implementations _may_ send trailing metadata as HTTP headers // instead. Envoy is the canonical implementation of the gRPC-Web protocol, // so we emulate Envoy's behavior and put the trailing metadata in the HTTP // headers. mergeHeaders(hc.responseWriter.Header(), mergedTrailers) return nil } if hc.web { // We're using gRPC-Web and we've already sent the headers, so we write // trailing metadata to the HTTP body. if err := hc.marshaler.MarshalWebTrailers(mergedTrailers); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } // We're using standard gRPC. Even if we haven't written to the body and // we're sending a "trailers-only" response, we must send trailing metadata // as HTTP trailers. (If we had frame-level control of the HTTP/2 layer, we // could send trailers-only responses as a single HEADER frame and no DATA // frames, but net/http doesn't expose APIs that low-level.) if !hc.wroteToBody { // This block works around a bug in x/net/http2. Until Go 1.20, trailers // written using http.TrailerPrefix were only sent if either (1) there's // data in the body, or (2) the innermost http.ResponseWriter is flushed. // To ensure that we always send a valid gRPC response, even if the user // has wrapped the response writer in net/http middleware that doesn't // implement http.Flusher, we must pre-declare our HTTP trailers. We can // remove this when Go 1.21 ships and we drop support for Go 1.19. for key := range mergedTrailers { addHeaderCanonical(hc.responseWriter.Header(), headerTrailer, key) } hc.responseWriter.WriteHeader(http.StatusOK) for key, values := range mergedTrailers { for _, value := range values { // These are potentially user-supplied, so we can't assume they're in // canonical form. Don't use addHeaderCanonical. hc.responseWriter.Header().Add(key, value) } } return nil } // In net/http's ResponseWriter API, we send HTTP trailers by writing to the // headers map with a special prefix. This prefixing is an implementation // detail, so we should hide it and _not_ mutate the user-visible headers. // // Note that this is _very_ finicky and difficult to test with net/http, // since correctness depends on low-level framing details. Breaking this // logic breaks Envoy's gRPC-Web translation. for key, values := range mergedTrailers { for _, value := range values { // These are potentially user-supplied, so we can't assume they're in // canonical form. Don't use addHeaderCanonical. hc.responseWriter.Header().Add(http.TrailerPrefix+key, value) } } return nil } type grpcMarshaler struct { envelopeWriter } func (m *grpcMarshaler) MarshalWebTrailers(trailer http.Header) *Error { raw := m.envelopeWriter.bufferPool.Get() defer m.envelopeWriter.bufferPool.Put(raw) for key, values := range trailer { // Per the Go specification, keys inserted during iteration may be produced // later in the iteration or may be skipped. For safety, avoid mutating the // map if the key is already lower-cased. lower := strings.ToLower(key) if key == lower { continue } delete(trailer, key) trailer[lower] = values } if err := trailer.Write(raw); err != nil { return errorf(CodeInternal, "format trailers: %w", err) } return m.Write(&envelope{ Data: raw, Flags: grpcFlagEnvelopeTrailer, }) } type grpcUnmarshaler struct { envelopeReader envelopeReader web bool webTrailer http.Header } func (u *grpcUnmarshaler) Unmarshal(message any) *Error { err := u.envelopeReader.Unmarshal(message) if err == nil { return nil } if !errors.Is(err, errSpecialEnvelope) { return err } env := u.envelopeReader.last if !u.web || !env.IsSet(grpcFlagEnvelopeTrailer) { return errorf(CodeInternal, "protocol error: invalid envelope flags %d", env.Flags) } // Per the gRPC-Web specification, trailers should be encoded as an HTTP/1 // headers block _without_ the terminating newline. To make the headers // parseable by net/textproto, we need to add the newline. if err := env.Data.WriteByte('\n'); err != nil { return errorf(CodeInternal, "unmarshal web trailers: %w", err) } bufferedReader := bufio.NewReader(env.Data) mimeReader := textproto.NewReader(bufferedReader) mimeHeader, mimeErr := mimeReader.ReadMIMEHeader() if mimeErr != nil { return errorf( CodeInternal, "gRPC-Web protocol error: trailers invalid: %w", mimeErr, ) } u.webTrailer = http.Header(mimeHeader) return errSpecialEnvelope } func (u *grpcUnmarshaler) WebTrailer() http.Header { return u.webTrailer } func grpcValidateResponse( response *http.Response, header, trailer http.Header, availableCompressors readOnlyCompressionPools, protobuf Codec, ) *Error { if response.StatusCode != http.StatusOK { return errorf(grpcHTTPToCode(response.StatusCode), "HTTP status %v", response.Status) } if compression := getHeaderCanonical(response.Header, grpcHeaderCompression); compression != "" && compression != compressionIdentity && !availableCompressors.Contains(compression) { // Per https://github.com/grpc/grpc/blob/master/doc/compression.md, we // should return CodeInternal and specify acceptable compression(s) (in // addition to setting the Grpc-Accept-Encoding header). return errorf( CodeInternal, "unknown encoding %q: accepted encodings are %v", compression, availableCompressors.CommaSeparatedNames(), ) } // When there's no body, gRPC and gRPC-Web servers may send error information // in the HTTP headers. if err := grpcErrorFromTrailer( protobuf, response.Header, ); err != nil && !errors.Is(err, errTrailersWithoutGRPCStatus) { // Per the specification, only the HTTP status code and Content-Type should // be treated as headers. The rest should be treated as trailing metadata. if contentType := getHeaderCanonical(response.Header, headerContentType); contentType != "" { setHeaderCanonical(header, headerContentType, contentType) } mergeHeaders(trailer, response.Header) delHeaderCanonical(trailer, headerContentType) // Also set the error metadata err.meta = header.Clone() mergeHeaders(err.meta, trailer) return err } // The response is valid, so we should expose the headers. mergeHeaders(header, response.Header) return nil } func grpcHTTPToCode(httpCode int) Code { // https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md // Note that this is not just the inverse of the gRPC-to-HTTP mapping. 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 } } // The gRPC wire protocol specifies that errors should be serialized using the // binary Protobuf format, even if the messages in the request/response stream // use a different codec. Consequently, this function needs a Protobuf codec to // unmarshal error information in the headers. func grpcErrorFromTrailer(protobuf Codec, trailer http.Header) *Error { codeHeader := getHeaderCanonical(trailer, grpcHeaderStatus) if codeHeader == "" { return NewError(CodeInternal, errTrailersWithoutGRPCStatus) } if codeHeader == "0" { return nil } code, err := strconv.ParseUint(codeHeader, 10 /* base */, 32 /* bitsize */) if err != nil { return errorf(CodeInternal, "protocol error: invalid error code %q", codeHeader) } message, err := grpcPercentDecode(getHeaderCanonical(trailer, grpcHeaderMessage)) if err != nil { return errorf(CodeInternal, "protocol error: invalid error message %q", message) } retErr := NewWireError(Code(code), errors.New(message)) detailsBinaryEncoded := getHeaderCanonical(trailer, grpcHeaderDetails) if len(detailsBinaryEncoded) > 0 { detailsBinary, err := DecodeBinaryHeader(detailsBinaryEncoded) if err != nil { return errorf(CodeInternal, "server returned invalid grpc-status-details-bin trailer: %w", err) } var status statusv1.Status if err := protobuf.Unmarshal(detailsBinary, &status); err != nil { return errorf(CodeInternal, "server returned invalid protobuf for error details: %w", err) } for _, d := range status.GetDetails() { retErr.details = append(retErr.details, &ErrorDetail{pb: d}) } // Prefer the Protobuf-encoded data to the headers (grpc-go does this too). retErr.code = Code(status.GetCode()) retErr.err = errors.New(status.GetMessage()) } return retErr } func grpcParseTimeout(timeout string) (time.Duration, error) { if timeout == "" { return 0, errNoTimeout } unit, err := grpcTimeoutUnitLookup(timeout[len(timeout)-1]) if err != nil { return 0, err } num, err := strconv.ParseInt(timeout[:len(timeout)-1], 10 /* base */, 64 /* bitsize */) if err != nil || num < 0 { return 0, fmt.Errorf("protocol error: invalid timeout %q", timeout) } if num > 99999999 { // timeout must be ASCII string of at most 8 digits return 0, fmt.Errorf("protocol error: timeout %q is too long", timeout) } const grpcTimeoutMaxHours = math.MaxInt64 / int64(time.Hour) // how many hours fit into a time.Duration? if unit == time.Hour && num > grpcTimeoutMaxHours { // Timeout is effectively unbounded, so ignore it. The grpc-go // implementation does the same thing. return 0, errNoTimeout } return time.Duration(num) * unit, nil } func grpcEncodeTimeout(timeout time.Duration) string { if timeout <= 0 { return "0n" } // The gRPC protocol limits timeouts to 8 characters (not counting the unit), // so timeouts must be strictly less than 1e8 of the appropriate unit. const grpcTimeoutMaxValue = 1e8 var ( size time.Duration unit byte ) switch { case timeout < time.Nanosecond*grpcTimeoutMaxValue: size, unit = time.Nanosecond, 'n' case timeout < time.Microsecond*grpcTimeoutMaxValue: size, unit = time.Microsecond, 'u' case timeout < time.Millisecond*grpcTimeoutMaxValue: size, unit = time.Millisecond, 'm' case timeout < time.Second*grpcTimeoutMaxValue: size, unit = time.Second, 'S' case timeout < time.Minute*grpcTimeoutMaxValue: size, unit = time.Minute, 'M' default: // time.Duration is an int64 number of nanoseconds, so the largest // expressible duration is less than 1e8 hours. size, unit = time.Hour, 'H' } buf := make([]byte, 0, 9) buf = strconv.AppendInt(buf, int64(timeout/size), 10 /* base */) buf = append(buf, unit) return string(buf) } func grpcTimeoutUnitLookup(unit byte) (time.Duration, error) { switch unit { case 'n': return time.Nanosecond, nil case 'u': return time.Microsecond, nil case 'm': return time.Millisecond, nil case 'S': return time.Second, nil case 'M': return time.Minute, nil case 'H': return time.Hour, nil default: return 0, fmt.Errorf("protocol error: timeout has invalid unit %q", unit) } } func grpcCodecFromContentType(web bool, contentType string) string { if (!web && contentType == grpcContentTypeDefault) || (web && contentType == grpcWebContentTypeDefault) { // implicitly protobuf return codecNameProto } prefix := grpcContentTypePrefix if web { prefix = grpcWebContentTypePrefix } return strings.TrimPrefix(contentType, prefix) } func grpcContentTypeFromCodecName(web bool, name string) string { if web { return grpcWebContentTypePrefix + name } return grpcContentTypePrefix + name } func grpcErrorToTrailer(trailer http.Header, protobuf Codec, err error) { if err == nil { setHeaderCanonical(trailer, grpcHeaderStatus, "0") // zero is the gRPC OK status setHeaderCanonical(trailer, grpcHeaderMessage, "") return } status := grpcStatusFromError(err) code := strconv.Itoa(int(status.GetCode())) bin, binErr := protobuf.Marshal(status) if binErr != nil { setHeaderCanonical( trailer, grpcHeaderStatus, strconv.FormatInt(int64(CodeInternal), 10 /* base */), ) setHeaderCanonical( trailer, grpcHeaderMessage, grpcPercentEncode( fmt.Sprintf("marshal protobuf status: %v", binErr), ), ) return } if connectErr, ok := asError(err); ok { mergeHeaders(trailer, connectErr.meta) } setHeaderCanonical(trailer, grpcHeaderStatus, code) setHeaderCanonical(trailer, grpcHeaderMessage, grpcPercentEncode(status.GetMessage())) setHeaderCanonical(trailer, grpcHeaderDetails, EncodeBinaryHeader(bin)) } func grpcStatusFromError(err error) *statusv1.Status { status := &statusv1.Status{ Code: int32(CodeUnknown), Message: err.Error(), } if connectErr, ok := asError(err); ok { status.Code = int32(connectErr.Code()) status.Message = connectErr.Message() status.Details = connectErr.detailsAsAny() } return status } // grpcPercentEncode follows RFC 3986 Section 2.1 and the gRPC HTTP/2 spec. // It's a variant of URL-encoding with fewer reserved characters. It's intended // to take UTF-8 encoded text and escape non-ASCII bytes so that they're valid // HTTP/1 headers, while still maximizing readability of the data on the wire. // // The grpc-message trailer (used for human-readable error messages) should be // percent-encoded. // // References: // // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses // https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 func grpcPercentEncode(msg string) string { var hexCount int for i := 0; i < len(msg); i++ { if grpcShouldEscape(msg[i]) { hexCount++ } } if hexCount == 0 { return msg } // We need to escape some characters, so we'll need to allocate a new string. var out strings.Builder out.Grow(len(msg) + 2*hexCount) for i := 0; i < len(msg); i++ { switch char := msg[i]; { case grpcShouldEscape(char): out.WriteByte('%') out.WriteByte(upperhex[char>>4]) out.WriteByte(upperhex[char&15]) default: out.WriteByte(char) } } return out.String() } func grpcPercentDecode(input string) (string, error) { percentCount := 0 for i := 0; i < len(input); { switch input[i] { case '%': percentCount++ if err := validateHex(input[i:]); err != nil { return "", err } i += 3 default: i++ } } if percentCount == 0 { return input, nil } // We need to unescape some characters, so we'll need to allocate a new string. var out strings.Builder out.Grow(len(input) - 2*percentCount) for i := 0; i < len(input); i++ { switch input[i] { case '%': out.WriteByte(unhex(input[i+1])<<4 | unhex(input[i+2])) i += 2 default: out.WriteByte(input[i]) } } return out.String(), nil } // Characters that need to be escaped are defined in gRPC's HTTP/2 spec. // They're different from the generic set defined in RFC 3986. func grpcShouldEscape(char byte) bool { return char < ' ' || char > '~' || char == '%' } func unhex(char byte) byte { switch { case '0' <= char && char <= '9': return char - '0' case 'a' <= char && char <= 'f': return char - 'a' + 10 case 'A' <= char && char <= 'F': return char - 'A' + 10 } return 0 } func isHex(char byte) bool { return ('0' <= char && char <= '9') || ('a' <= char && char <= 'f') || ('A' <= char && char <= 'F') } func validateHex(input string) error { if len(input) < 3 || input[0] != '%' || !isHex(input[1]) || !isHex(input[2]) { if len(input) > 3 { input = input[:3] } return fmt.Errorf("invalid percent-encoded string %q", input) } return nil } connect-go-1.13.0/protocol_grpc_test.go000066400000000000000000000151261453471351600201100ustar00rootroot00000000000000// Copyright 2021-2023 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" "math" "net/http" "net/http/httptest" "strings" "testing" "testing/quick" "time" "unicode/utf8" "connectrpc.com/connect/internal/assert" "github.com/google/go-cmp/cmp" ) func TestGRPCHandlerSender(t *testing.T) { t.Parallel() newConn := func(web bool) *grpcHandlerConn { responseWriter := httptest.NewRecorder() protobufCodec := &protoBinaryCodec{} bufferPool := newBufferPool() request, err := http.NewRequest( http.MethodPost, "https://demo.example.com", strings.NewReader(""), ) assert.Nil(t, err) return &grpcHandlerConn{ spec: Spec{}, web: web, bufferPool: bufferPool, protobuf: protobufCodec, marshaler: grpcMarshaler{ envelopeWriter: envelopeWriter{ sender: writeSender{writer: responseWriter}, codec: protobufCodec, bufferPool: bufferPool, }, }, responseWriter: responseWriter, responseHeader: make(http.Header), responseTrailer: make(http.Header), request: request, unmarshaler: grpcUnmarshaler{ envelopeReader: envelopeReader{ reader: request.Body, codec: protobufCodec, bufferPool: bufferPool, }, }, } } t.Run("web", func(t *testing.T) { t.Parallel() testGRPCHandlerConnMetadata(t, newConn(true)) }) t.Run("http2", func(t *testing.T) { t.Parallel() testGRPCHandlerConnMetadata(t, newConn(false)) }) } func testGRPCHandlerConnMetadata(t *testing.T, conn handlerConnCloser) { // Closing the sender shouldn't unpredictably mutate user-visible headers or // trailers. t.Helper() expectHeaders := conn.ResponseHeader().Clone() expectTrailers := conn.ResponseTrailer().Clone() conn.Close(NewError(CodeUnavailable, errors.New("oh no"))) if diff := cmp.Diff(expectHeaders, conn.ResponseHeader()); diff != "" { t.Errorf("headers changed:\n%s", diff) } gotTrailers := conn.ResponseTrailer() if diff := cmp.Diff(expectTrailers, gotTrailers); diff != "" { t.Errorf("trailers changed:\n%s", diff) } } func TestGRPCParseTimeout(t *testing.T) { t.Parallel() _, err := grpcParseTimeout("") assert.True(t, errors.Is(err, errNoTimeout)) _, err = grpcParseTimeout("foo") assert.NotNil(t, err) _, err = grpcParseTimeout("12xS") assert.NotNil(t, err) _, err = grpcParseTimeout("999999999n") // 9 digits assert.NotNil(t, err) assert.False(t, errors.Is(err, errNoTimeout)) _, err = grpcParseTimeout("99999999H") // 8 digits but overflows time.Duration assert.True(t, errors.Is(err, errNoTimeout)) duration, err := grpcParseTimeout("45S") assert.Nil(t, err) assert.Equal(t, duration, 45*time.Second) const long = "99999999S" duration, err = grpcParseTimeout(long) // 8 digits, shouldn't overflow assert.Nil(t, err) assert.Equal(t, duration, 99999999*time.Second) } func TestGRPCEncodeTimeout(t *testing.T) { t.Parallel() timeout := grpcEncodeTimeout(time.Hour + time.Second) assert.Equal(t, timeout, "3601000m") // NB, m is milliseconds // overflow and underflow timeout = grpcEncodeTimeout(time.Duration(math.MaxInt64)) assert.Equal(t, timeout, "2562047H") timeout = grpcEncodeTimeout(-1) assert.Equal(t, timeout, "0n") timeout = grpcEncodeTimeout(-1 * time.Hour) assert.Equal(t, timeout, "0n") // unit conversions const eightDigitsNanos = 99999999 * time.Nanosecond timeout = grpcEncodeTimeout(eightDigitsNanos) // shouldn't need unit conversion assert.Equal(t, timeout, "99999999n") timeout = grpcEncodeTimeout(eightDigitsNanos + 1) // 9 digits, convert to micros assert.Equal(t, timeout, "100000u") // rounding timeout = grpcEncodeTimeout(10*time.Millisecond + 1) // shouldn't round assert.Equal(t, timeout, "10000001n") timeout = grpcEncodeTimeout(10*time.Second + 1) // should round down assert.Equal(t, timeout, "10000000u") } func TestGRPCPercentEncodingQuick(t *testing.T) { t.Parallel() roundtrip := func(input string) bool { if !utf8.ValidString(input) { return true } encoded := grpcPercentEncode(input) decoded, err := grpcPercentDecode(encoded) return err == nil && decoded == input } if err := quick.Check(roundtrip, nil /* config */); err != nil { t.Error(err) } } func TestGRPCPercentEncoding(t *testing.T) { t.Parallel() roundtrip := func(input string) { assert.True(t, utf8.ValidString(input), assert.Sprintf("input invalid UTF-8")) encoded := grpcPercentEncode(input) t.Logf("%q encoded as %q", input, encoded) decoded, err := grpcPercentDecode(encoded) assert.Nil(t, err) assert.Equal(t, decoded, input) } roundtrip("foo") roundtrip("foo bar") roundtrip(`foo%bar`) roundtrip("fiancée") } func TestGRPCWebTrailerMarshalling(t *testing.T) { t.Parallel() responseWriter := httptest.NewRecorder() marshaler := grpcMarshaler{ envelopeWriter: envelopeWriter{ sender: writeSender{writer: responseWriter}, bufferPool: newBufferPool(), }, } trailer := http.Header{} trailer.Add("grpc-status", "0") trailer.Add("Grpc-Message", "Foo") trailer.Add("User-Provided", "bar") err := marshaler.MarshalWebTrailers(trailer) assert.Nil(t, err) responseWriter.Body.Next(5) // skip flags and message length marshalled := responseWriter.Body.String() assert.Equal(t, marshalled, "grpc-message: Foo\r\ngrpc-status: 0\r\nuser-provided: bar\r\n") } func BenchmarkGRPCPercentEncoding(b *testing.B) { input := "Hello, 世界" want := "Hello, %E4%B8%96%E7%95%8C" b.ReportAllocs() for i := 0; i < b.N; i++ { got := grpcPercentEncode(input) if got != want { b.Fatalf("grpcPercentEncode(%q) = %s, want %s", input, got, want) } } } func BenchmarkGRPCPercentDecoding(b *testing.B) { input := "Hello, %E4%B8%96%E7%95%8C" want := "Hello, 世界" b.ReportAllocs() for i := 0; i < b.N; i++ { got, _ := grpcPercentDecode(input) if got != want { b.Fatalf("grpcPercentDecode(%q) = %s, want %s", input, got, want) } } } func BenchmarkGRPCTimeoutEncoding(b *testing.B) { input := time.Second * 45 want := "45000000u" b.ReportAllocs() for i := 0; i < b.N; i++ { got := grpcEncodeTimeout(input) if got != want { b.Fatalf("grpcEncodeTimeout(%q) = %s, want %s", input, got, want) } } } connect-go-1.13.0/protocol_test.go000066400000000000000000000037171453471351600171000ustar00rootroot00000000000000// Copyright 2021-2023 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 ( "testing" "connectrpc.com/connect/internal/assert" ) func TestCanonicalizeContentType(t *testing.T) { t.Parallel() tests := []struct { name string arg string want string }{ {name: "uppercase should be normalized", arg: "APPLICATION/json", want: "application/json"}, {name: "charset param should be treated as lowercase", arg: "application/json; charset=UTF-8", want: "application/json; charset=utf-8"}, {name: "non charset param should not be changed", arg: "multipart/form-data; boundary=fooBar", want: "multipart/form-data; boundary=fooBar"}, {name: "no parameters should be normalized", arg: "APPLICATION/json; ", want: "application/json"}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() assert.Equal(t, canonicalizeContentType(tt.arg), tt.want) }) } } func BenchmarkCanonicalizeContentType(b *testing.B) { b.Run("simple", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = canonicalizeContentType("application/json") } b.ReportAllocs() }) b.Run("with charset", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = canonicalizeContentType("application/json; charset=utf-8") } b.ReportAllocs() }) b.Run("with other param", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = canonicalizeContentType("application/json; foo=utf-8") } b.ReportAllocs() }) } connect-go-1.13.0/recover.go000066400000000000000000000046561453471351600156500ustar00rootroot00000000000000// Copyright 2021-2023 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. // // This interceptor uses a somewhat unusual strategy to recover from panics. // The standard recovery idiom: // // if r := recover(); r != nil { ... } // // isn't robust in the face of user error, because it doesn't handle // panic(nil). This occasionally happens by mistake, and it's a beast to debug // without a more robust idiom. See https://github.com/golang/go/issues/25448 // for details. 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) } panicked := true defer func() { if panicked { r := recover() // 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) panicked = false return res, err } } func (i *recoverHandlerInterceptor) WrapStreamingHandler(next StreamingHandlerFunc) StreamingHandlerFunc { return func(ctx context.Context, conn StreamingHandlerConn) (retErr error) { panicked := true defer func() { if panicked { r := recover() // 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, Spec{}, nil, r) } }() err := next(ctx, conn) panicked = false return err } } connect-go-1.13.0/recover_ext_test.go000066400000000000000000000064451453471351600175650ustar00rootroot00000000000000// Copyright 2021-2023 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_test import ( "context" "fmt" "net/http" "testing" connect "connectrpc.com/connect" "connectrpc.com/connect/internal/assert" pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" "connectrpc.com/connect/internal/memhttp/memhttptest" ) type panicPingServer struct { pingv1connect.UnimplementedPingServiceHandler panicWith any } func (s *panicPingServer) Ping( context.Context, *connect.Request[pingv1.PingRequest], ) (*connect.Response[pingv1.PingResponse], error) { panic(s.panicWith) //nolint:forbidigo } func (s *panicPingServer) CountUp( _ context.Context, _ *connect.Request[pingv1.CountUpRequest], stream *connect.ServerStream[pingv1.CountUpResponse], ) error { if err := stream.Send(&pingv1.CountUpResponse{}); err != nil { return err } panic(s.panicWith) //nolint:forbidigo } func TestWithRecover(t *testing.T) { t.Parallel() handle := func(_ context.Context, _ connect.Spec, _ http.Header, r any) error { return connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("panic: %v", r)) } assertHandled := func(err error) { t.Helper() assert.NotNil(t, err) assert.Equal(t, connect.CodeOf(err), connect.CodeFailedPrecondition) } assertNotHandled := func(err error) { t.Helper() // When HTTP/2 handlers panic, net/http sends an RST_STREAM frame with code // INTERNAL_ERROR. We should be mapping this back to CodeInternal. assert.Equal(t, connect.CodeOf(err), connect.CodeInternal) } drainStream := func(stream *connect.ServerStreamForClient[pingv1.CountUpResponse]) error { t.Helper() defer stream.Close() assert.True(t, stream.Receive()) // expect one response msg assert.False(t, stream.Receive()) // expect panic before second response msg return stream.Err() } pinger := &panicPingServer{} mux := http.NewServeMux() mux.Handle(pingv1connect.NewPingServiceHandler(pinger, connect.WithRecover(handle))) server := memhttptest.NewServer(t, mux) client := pingv1connect.NewPingServiceClient( server.Client(), server.URL(), ) for _, panicWith := range []any{42, nil} { pinger.panicWith = panicWith _, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{})) assertHandled(err) stream, err := client.CountUp(context.Background(), connect.NewRequest(&pingv1.CountUpRequest{})) assert.Nil(t, err) assertHandled(drainStream(stream)) } pinger.panicWith = http.ErrAbortHandler _, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{})) assertNotHandled(err) stream, err := client.CountUp(context.Background(), connect.NewRequest(&pingv1.CountUpRequest{})) assert.Nil(t, err) assertNotHandled(drainStream(stream)) }