├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── commit.yaml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── RATIONALE.md ├── README.md ├── api ├── api.go ├── handler │ ├── features.go │ ├── features_test.go │ ├── handler.go │ └── wasm.go └── wasm.go ├── examples ├── auth.wasm ├── auth.wat ├── config.wasm ├── config.wat ├── log.wasm ├── log.wat ├── redact.wasm ├── redact.wat ├── router.wasm ├── router.wat ├── wasi.wasm └── wasi.wat ├── go.mod ├── go.sum ├── handler ├── cstring.go ├── middleware.go ├── middleware_test.go ├── nethttp │ ├── benchmark_test.go │ ├── buffer.go │ ├── buffer_test.go │ ├── example_test.go │ ├── host.go │ ├── host_test.go │ ├── middleware.go │ ├── middleware_test.go │ └── tck_test.go ├── options.go └── state.go ├── internal └── test │ ├── testdata.go │ └── testdata │ ├── bench │ ├── add_header_value.wasm │ ├── add_header_value.wat │ ├── get_header_names.wasm │ ├── get_header_names.wat │ ├── get_header_values.wasm │ ├── get_header_values.wat │ ├── get_uri.wasm │ ├── get_uri.wat │ ├── log.wasm │ ├── log.wat │ ├── read_body.wasm │ ├── read_body.wat │ ├── read_body_stream.wasm │ ├── read_body_stream.wat │ ├── remove_header.wasm │ ├── remove_header.wat │ ├── set_header_value.wasm │ ├── set_header_value.wat │ ├── set_status_code.wasm │ ├── set_status_code.wat │ ├── set_uri.wasm │ ├── set_uri.wat │ ├── write_body.wasm │ └── write_body.wat │ ├── e2e │ ├── handle_response.wasm │ ├── handle_response.wat │ ├── header_names.wasm │ ├── header_names.wat │ ├── header_value.wasm │ ├── header_value.wat │ ├── method.wasm │ ├── method.wat │ ├── protocol_version.wasm │ ├── protocol_version.wat │ ├── uri.wasm │ └── uri.wat │ └── error │ ├── panic_on_handle_request.wasm │ ├── panic_on_handle_request.wat │ ├── panic_on_handle_response.wasm │ ├── panic_on_handle_response.wat │ ├── panic_on_start.wasm │ ├── panic_on_start.wat │ ├── set_request_header_after_next.wasm │ └── set_request_header_after_next.wat ├── tck ├── README.md ├── backend.go ├── guest │ ├── go.mod │ ├── go.sum │ └── main.go ├── run.go └── tck.wasm └── testing └── handlertest └── testhandler.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Improves experience of commands like `make format` on Windows 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/commit.yaml: -------------------------------------------------------------------------------- 1 | # `name` value will appear "as is" in the badge. 2 | # See https://docs.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow#adding-a-workflow-status-badge-to-your-repository 3 | # yamllint --format github .github/workflows/commit.yaml 4 | --- 5 | name: "build" 6 | 7 | on: 8 | push: # We run tests on non-tagged pushes to main 9 | tags: '' 10 | branches: main 11 | paths-ignore: 12 | - '**/*.md' 13 | pull_request: # We also run tests on pull requests targeted at the main branch. 14 | branches: main 15 | paths-ignore: 16 | - '**/*.md' 17 | # workflow_dispatch will let us manually trigger the workflow from GitHub actions dashboard. 18 | # For example, you can try to build a branch without raising a pull request. 19 | # See https://docs.github.com/en/free-pro-team@latest/actions/managing-workflow-runs/manually-running-a-workflow 20 | workflow_dispatch: 21 | 22 | jobs: 23 | check: 24 | runs-on: ubuntu-22.04 25 | strategy: 26 | matrix: 27 | go-version: # Note: Go only supports 2 versions: https://go.dev/doc/devel/release#policy 28 | - "1.22" 29 | - "1.23" 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - uses: actions/setup-go@v4 35 | with: # not cache: true as we also need to cache golint 36 | go-version: ${{ matrix.go-version }} 37 | cache: false 38 | 39 | - uses: actions/cache@v3 40 | with: 41 | path: | 42 | ~/.cache/go-build 43 | ~/.cache/golangci-lint 44 | ~/go/pkg/mod 45 | ~/go/bin 46 | key: check-${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum', 'Makefile') }} 47 | 48 | - name: "Set up wat2wasm" 49 | run: | # Needed for testdata. wabt includes wat2wasm. 50 | wabt_version=1.0.33 51 | wabt_url=https://github.com/WebAssembly/wabt/releases/download/${wabt_version}/wabt-${wabt_version}-ubuntu.tar.gz 52 | curl -sSL ${wabt_url} | tar --strip-components 2 -C /usr/local/bin -xzf - wabt-${wabt_version}/bin/wat2wasm 53 | 54 | - run: make testdata 55 | 56 | - run: make check 57 | 58 | test: 59 | runs-on: ubuntu-22.04 60 | strategy: 61 | matrix: 62 | go-version: # Note: Go only supports 2 versions: https://go.dev/doc/devel/release#policy 63 | - "1.22" 64 | - "1.23" 65 | 66 | steps: 67 | - name: "Checkout" 68 | uses: actions/checkout@v4 69 | 70 | - name: "Set up Go" 71 | uses: actions/setup-go@v4 72 | with: 73 | go-version: ${{ matrix.go-version }} 74 | cache: true 75 | 76 | - name: "Test" 77 | run: make test 78 | 79 | # TinyGo is not idempotent when generating wasm, so we don't check 80 | # in %.wasm as a part of this job. If an updated binary wasn't checked 81 | # in, the test job will fail, so here we just want to make sure the TCK 82 | # does not have any build failures. 83 | build-tck: 84 | runs-on: ubuntu-22.04 85 | steps: 86 | - name: "Checkout" 87 | uses: actions/checkout@v4 88 | 89 | - name: "Set up Go" 90 | uses: actions/setup-go@v4 91 | with: 92 | go-version: "1.23" 93 | cache: true 94 | 95 | - name: "Set up TinyGo" 96 | uses: acifani/setup-tinygo@v1 97 | with: 98 | tinygo-version: 0.33.0 99 | 100 | - name: "Cache TinyGo build" 101 | uses: actions/cache@v3 102 | with: 103 | path: ~/.cache/tinygo 104 | key: tinygo-0.33.0 105 | 106 | - name: "Build TCK" 107 | run: make tck 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | # Goland 24 | .idea 25 | 26 | # codecov.io 27 | /coverage.txt 28 | 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This is built using - https://golangci-lint.run/usage/configuration/ 2 | run: 3 | go: '1.21' 4 | modules-download-mode: readonly 5 | tests: false 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | gofumpt := mvdan.cc/gofumpt@v0.5.0 2 | gosimports := github.com/rinchsan/gosimports/cmd/gosimports@v0.3.8 3 | golangci_lint := github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0 4 | 5 | .PHONY: testdata 6 | testdata: 7 | @$(MAKE) build.wat 8 | 9 | wat_sources := $(wildcard examples/*.wat) $(wildcard internal/test/testdata/*/*.wat) 10 | build.wat: $(wat_sources) 11 | @for f in $^; do \ 12 | wasm=$$(echo $$f | sed -e 's/\.wat/\.wasm/'); \ 13 | wat2wasm -o $$wasm --debug-names $$f; \ 14 | done 15 | 16 | .PHONY: test 17 | test: 18 | @go test -v ./... 19 | 20 | .PHONY: bench 21 | bench: 22 | @(cd handler/nethttp; go test -run=NONE -bench=. .) 23 | 24 | golangci_lint_path := $(shell go env GOPATH)/bin/golangci-lint 25 | 26 | $(golangci_lint_path): 27 | @go install $(golangci_lint) 28 | 29 | .PHONY: lint 30 | lint: $(golangci_lint_path) 31 | @CGO_ENABLED=0 $(golangci_lint_path) run --timeout 5m 32 | 33 | .PHONY: format 34 | format: 35 | @go run $(gofumpt) -l -w . 36 | @go run $(gosimports) -local github.com/http-wasm/ -w $(shell find . -name '*.go' -type f) 37 | 38 | .PHONY: check 39 | check: 40 | @$(MAKE) lint 41 | @$(MAKE) format 42 | @go mod tidy 43 | @if [ ! -z "`git status -s`" ]; then \ 44 | echo "The following differences will fail CI until committed:"; \ 45 | git diff --exit-code; \ 46 | fi 47 | 48 | .PHONY: clean 49 | clean: ## Ensure a clean build 50 | @go clean -testcache 51 | 52 | # note: the guest wasm is stored in tck/, not tck/guest, so that go:embed can read it. 53 | .PHONY: tck 54 | tck: 55 | @cd tck/guest && tinygo build -o ../tck.wasm -scheduler=none --no-debug -target=wasi . 56 | -------------------------------------------------------------------------------- /RATIONALE.md: -------------------------------------------------------------------------------- 1 | # Notable rationale of http-wasm-host-go 2 | 3 | ## Guest pool 4 | 5 | Wasm guests may have static initialization logic that takes significant time, 6 | for example parsing a config file. Because Wasm is not thread-safe, we cannot 7 | use a single instance to handle requests concurrently and instead have a pool 8 | of initialized guests. If guests were used concurrently, runtime code compiled 9 | to wasm, such as stack pointer tracking and garbage collection, could enter an 10 | undefined state and panic/trap. 11 | 12 | The pool size is uncapped - any pool size cap would be a concurrency limit on 13 | the request handler. Production HTTP servers all have a native concept of 14 | concurrency limiting, so it would be redundant to have another way to limit 15 | concurrency here. 16 | 17 | It is understood that guests use more memory than the same logic in native 18 | code. For example, some compilers default to a minimum of 16MB, and there is 19 | also overhead for the VM running the guest. This can imply running into a 20 | resource constraint faster than the same logic in native code. However, it 21 | is remains a better choice to address this with your HTTP server's 22 | concurrency limit mechanism. 23 | 24 | ## Guest pinning 25 | 26 | As mentioned in the section above, guests (user-defined handlers compiled to 27 | wasm) are used serially. That said, the request lifecycle must be pinned to 28 | the same guest instance (VM). Pinning ensures any state assigned to the guest 29 | controlled request ID during `handle_request` is visible during 30 | `handle_response`. If a random guest in the pool was used for response 31 | handling, not only might the response side miss data, but it may also miss 32 | cleanup of it. 33 | 34 | Technically, pinning can occur via any means available on the host: context 35 | propagation, a thread safe map with lookup, or even returning a scoped response 36 | handler. The handler middleware uses an "out context" to implement this, which 37 | hides the underlying guest pool. In net/http using this is simple as you only 38 | need to pass that to the response side. The mosn stream handler has a filter 39 | per request, and the "out context" is stored as a field in that type. This 40 | allows asynchronous handling to use it. 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/http-wasm/http-wasm-host-go/workflows/build/badge.svg)](https://github.com/http-wasm/http-wasm-host-go) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/http-wasm/http-wasm-host-go)](https://goreportcard.com/report/github.com/http-wasm/http-wasm-host-go) 3 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) 4 | 5 | # http-wasm Host Library for Go 6 | 7 | [http-wasm][1] defines HTTP functions implemented in [WebAssembly][2]. This 8 | repository includes [http_handler ABI][3] middleware for various HTTP server 9 | libraries written in Go. 10 | 11 | * [nethttp](handler/nethttp): [net/http Handler][4] 12 | 13 | # WARNING: This is an early draft 14 | 15 | The current maturity phase is early draft. Once this is integrated with 16 | [coraza][5] and [dapr][6], we can begin discussions about compatability. 17 | 18 | [1]: https://github.com/http-wasm 19 | [2]: https://webassembly.org/ 20 | [3]: https://http-wasm.io/http-handler-abi/ 21 | [4]: https://pkg.go.dev/net/http#Handler 22 | [5]: https://github.com/corazawaf/coraza-http-wasm 23 | [6]: https://github.com/dapr/components-contrib/ 24 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // LogLevel controls the volume of logging. The lower the number the more 9 | // detail is logged. 10 | // 11 | // Note: The most voluminous level, LogLevelDebug is -1 to prevent users from 12 | // accidentally defaulting to it. 13 | type LogLevel int32 14 | 15 | const ( 16 | LogLevelDebug LogLevel = -1 17 | LogLevelInfo LogLevel = 0 18 | LogLevelWarn LogLevel = 1 19 | LogLevelError LogLevel = 2 20 | LogLevelNone LogLevel = 3 21 | ) 22 | 23 | // compile-time check to ensure NoopLogger implements api.Logger. 24 | var _ Logger = NoopLogger{} 25 | 26 | // NoopLogger is a convenience which ignores all output. 27 | type NoopLogger struct{} 28 | 29 | // IsEnabled implements the same method as documented on api.Logger. 30 | func (NoopLogger) IsEnabled(level LogLevel) bool { 31 | return level == LogLevelNone 32 | } 33 | 34 | // Log implements the same method as documented on api.Logger. 35 | func (NoopLogger) Log(context.Context, LogLevel, string) {} 36 | 37 | // compile-time check to ensure ConsoleLogger implements api.Logger. 38 | var _ Logger = ConsoleLogger{} 39 | 40 | // ConsoleLogger is a convenience which writes anything above LogLevelInfo to os.Stdout. 41 | type ConsoleLogger struct{} 42 | 43 | // IsEnabled implements the same method as documented on api.Logger. 44 | func (ConsoleLogger) IsEnabled(level LogLevel) bool { 45 | return level >= LogLevelInfo 46 | } 47 | 48 | // Log implements the same method as documented on api.Logger. 49 | func (ConsoleLogger) Log(_ context.Context, level LogLevel, message string) { 50 | if level >= LogLevelInfo { 51 | fmt.Println(message) 52 | } 53 | } 54 | 55 | type Logger interface { 56 | // IsEnabled returns true if the input level would be logged. 57 | // 58 | // Note: Guests may cache this value at requestscope. 59 | IsEnabled(LogLevel) bool 60 | 61 | // Log logs a message to the host's logs. 62 | Log(context.Context, LogLevel, string) 63 | } 64 | 65 | type Closer interface { 66 | // Close releases resources such as any Wasm modules, compiled code, and 67 | // the runtime. 68 | Close(context.Context) error 69 | } 70 | -------------------------------------------------------------------------------- /api/handler/features.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Features is a bit flag of features a host may support. 8 | // 9 | // Note: Numeric values are not intended to be interpreted except as bit flags. 10 | type Features uint32 11 | 12 | const ( 13 | // FeatureBufferRequest buffers the HTTP request body when reading, so that 14 | // FuncNext can see the original. 15 | // 16 | // Note: Buffering a request is done on the host and can use resources 17 | // such as memory. It also may reduce the features of the underlying 18 | // request due to implications of buffering or wrapping. 19 | FeatureBufferRequest Features = 1 << iota 20 | 21 | // FeatureBufferResponse buffers the HTTP response produced by FuncNext 22 | // instead of sending it immediately. This allows the caller to inspect and 23 | // overwrite the HTTP status code, response body or trailers. As the 24 | // response is deferred, expect timing differences when enabled. 25 | // 26 | // Note: Buffering a response is done on the host and can use resources 27 | // such as memory. It also may reduce the features of the underlying 28 | // response due to implications of buffering or wrapping. 29 | FeatureBufferResponse 30 | 31 | // FeatureTrailers allows guests to act differently depending on if the 32 | // host supports HTTP trailing headers (trailers) or not. 33 | // 34 | // # Handling unsupported 35 | // 36 | // This is a feature flag because trailers are not well-supported. For 37 | // example, mosn does not support trailers. 38 | // 39 | // A host that doesn't support trailers must do the following: 40 | // - return 0 for this bit in the FuncEnableFeatures result. 41 | // - return no trailer names or values. 42 | // - panic/trap on any call to set a trailer value. 43 | // 44 | // For example, a logging handler may be fine without trailers, while a 45 | // gRPC handler should err as it needs to access the gRPC status trailer. A 46 | // guest that requires trailers can fail during initialization instead of 47 | // per-request via inspecting the result of FuncEnableFeatures. 48 | // 49 | // See https://peps.python.org/pep-0444/#request-trailers-and-chunked-transfer-encoding 50 | FeatureTrailers 51 | ) 52 | 53 | // WithEnabled enables the feature or group of features. 54 | func (f Features) WithEnabled(feature Features) Features { 55 | return f | feature 56 | } 57 | 58 | // IsEnabled returns true if the feature (or group of features) is enabled. 59 | func (f Features) IsEnabled(feature Features) bool { 60 | return f&feature != 0 61 | } 62 | 63 | // String implements fmt.Stringer by returning each enabled feature. 64 | func (f Features) String() string { 65 | var builder strings.Builder 66 | for i := 0; i <= 31; i++ { // cycle through all bits to reduce code and maintenance 67 | target := Features(1 << i) 68 | if f.IsEnabled(target) { 69 | if name := featureName(target); name != "" { 70 | if builder.Len() > 0 { 71 | builder.WriteByte('|') 72 | } 73 | builder.WriteString(name) 74 | } 75 | } 76 | } 77 | return builder.String() 78 | } 79 | 80 | func featureName(f Features) string { 81 | switch f { 82 | case FeatureBufferRequest: 83 | return "buffer_request" 84 | case FeatureBufferResponse: 85 | return "buffer_response" 86 | case FeatureTrailers: 87 | return "trailers" 88 | } 89 | return "" 90 | } 91 | -------------------------------------------------------------------------------- /api/handler/features_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // TestFeatures_ZeroIsInvalid reminds maintainers that a bitset cannot use zero as a flag! 8 | // This is why we start iota with 1. 9 | func TestFeatures_ZeroIsInvalid(t *testing.T) { 10 | f := Features(0) 11 | f = f.WithEnabled(0) 12 | 13 | if f.IsEnabled(0) { 14 | t.Errorf("expected zero to not be enabled") 15 | } 16 | } 17 | 18 | // TestFeatures tests the bitset works as expected 19 | func TestFeatures(t *testing.T) { 20 | tests := []struct { 21 | name string 22 | feature Features 23 | }{ 24 | { 25 | name: "one is the smallest flag", 26 | feature: 1, 27 | }, 28 | { 29 | name: "31 is the largest feature flag", // because uint32 30 | feature: 1 << 31, 31 | }, 32 | } 33 | 34 | for _, tt := range tests { 35 | tc := tt 36 | t.Run(tc.name, func(t *testing.T) { 37 | f := Features(0) 38 | 39 | // Defaults to false 40 | if f.IsEnabled(tc.feature) { 41 | t.Errorf("expected %v to not be enabled", tc.feature) 42 | } 43 | 44 | // Set true makes it true 45 | f = f.WithEnabled(tc.feature) 46 | if !f.IsEnabled(tc.feature) { 47 | t.Errorf("expected %v to be enabled", tc.feature) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestFeatures_String(t *testing.T) { 54 | tests := []struct { 55 | name string 56 | feature Features 57 | expected string 58 | }{ 59 | {name: "none", feature: 0, expected: ""}, 60 | {name: "buffer_request", feature: FeatureBufferRequest, expected: "buffer_request"}, 61 | {name: "buffer_response", feature: FeatureBufferResponse, expected: "buffer_response"}, 62 | {name: "trailers", feature: FeatureTrailers, expected: "trailers"}, 63 | {name: "all", feature: FeatureBufferRequest | FeatureBufferResponse | FeatureTrailers, expected: "buffer_request|buffer_response|trailers"}, 64 | {name: "undefined", feature: 1 << 31, expected: ""}, 65 | } 66 | 67 | for _, tt := range tests { 68 | tc := tt 69 | t.Run(tc.name, func(t *testing.T) { 70 | if want, have := tc.expected, tc.feature.String(); want != have { 71 | t.Errorf("unexpected string, want: %q, have: %q", want, have) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /api/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/http-wasm/http-wasm-host-go/api" 8 | ) 9 | 10 | // Middleware is a factory of handler instances implemented in Wasm. 11 | type Middleware[H any] interface { 12 | // NewHandler creates an HTTP server handler implemented by a WebAssembly 13 | // module. The returned handler will not invoke FuncNext when it constructs 14 | // a response in guest wasm for reasons such as an authorization failure or 15 | // serving from cache. 16 | // 17 | // ## Notes 18 | // - Each handler is independent, so they don't share memory. 19 | // - Handlers returned are not safe for concurrent use. 20 | NewHandler(ctx context.Context, next H) H 21 | 22 | api.Closer 23 | } 24 | 25 | // Host supports the host side of the WebAssembly module named HostModule. 26 | // These callbacks are used by the guest function export FuncHandleRequest. 27 | type Host interface { 28 | // EnableFeatures supports the WebAssembly function export EnableFeatures. 29 | EnableFeatures(ctx context.Context, features Features) Features 30 | 31 | // GetMethod supports the WebAssembly function export FuncGetMethod. 32 | GetMethod(ctx context.Context) string 33 | 34 | // SetMethod supports the WebAssembly function export FuncSetMethod. 35 | SetMethod(ctx context.Context, method string) 36 | 37 | // GetURI supports the WebAssembly function export FuncGetURI. 38 | GetURI(ctx context.Context) string 39 | 40 | // SetURI supports the WebAssembly function export FuncSetURI. 41 | SetURI(ctx context.Context, uri string) 42 | 43 | // GetProtocolVersion supports the WebAssembly function export 44 | // FuncGetProtocolVersion. 45 | GetProtocolVersion(ctx context.Context) string 46 | 47 | // GetRequestHeaderNames supports the WebAssembly function export 48 | // FuncGetHeaderNames with HeaderKindRequest. This returns nil if no 49 | // headers exist. 50 | GetRequestHeaderNames(ctx context.Context) []string 51 | 52 | // GetRequestHeaderValues supports the WebAssembly function export 53 | // FuncGetHeaderValues with HeaderKindRequest. This returns nil if no 54 | // values exist. 55 | GetRequestHeaderValues(ctx context.Context, name string) []string 56 | 57 | // SetRequestHeaderValue supports the WebAssembly function export 58 | // FuncSetHeaderValue with HeaderKindRequest. 59 | SetRequestHeaderValue(ctx context.Context, name, value string) 60 | 61 | // AddRequestHeaderValue supports the WebAssembly function export 62 | // FuncAddHeaderValue with HeaderKindRequest. 63 | AddRequestHeaderValue(ctx context.Context, name, value string) 64 | 65 | // RemoveRequestHeader supports the WebAssembly function export 66 | // FuncRemoveHeader with HeaderKindRequest. 67 | RemoveRequestHeader(ctx context.Context, name string) 68 | 69 | // RequestBodyReader supports the WebAssembly function export 70 | // FuncReadBody with BodyKindRequest. 71 | RequestBodyReader(ctx context.Context) io.ReadCloser 72 | 73 | // RequestBodyWriter supports the WebAssembly function export 74 | // FuncWriteBody with BodyKindRequest. 75 | RequestBodyWriter(ctx context.Context) io.Writer 76 | 77 | // GetRequestTrailerNames supports the WebAssembly function export 78 | // FuncGetHeaderNames with HeaderKindRequestTrailers. This returns nil if 79 | // no trailers exist or FeatureTrailers is not supported. 80 | GetRequestTrailerNames(ctx context.Context) []string 81 | 82 | // GetRequestTrailerValues supports the WebAssembly function export 83 | // FuncGetHeaderValues with HeaderKindRequestTrailers. This returns nil if 84 | // no values exist or FeatureTrailers is not supported. 85 | GetRequestTrailerValues(ctx context.Context, name string) []string 86 | 87 | // SetRequestTrailerValue supports the WebAssembly function export 88 | // FuncSetHeaderValue with HeaderKindRequestTrailers. This panics if 89 | // FeatureTrailers is not supported. 90 | SetRequestTrailerValue(ctx context.Context, name, value string) 91 | 92 | // AddRequestTrailerValue supports the WebAssembly function export 93 | // FuncAddHeaderValue with HeaderKindRequestTrailers. This panics if 94 | // FeatureTrailers is not supported. 95 | AddRequestTrailerValue(ctx context.Context, name, value string) 96 | 97 | // RemoveRequestTrailer supports the WebAssembly function export 98 | // FuncRemoveHeader with HeaderKindRequestTrailers. This panics if 99 | // FeatureTrailers is not supported. 100 | RemoveRequestTrailer(ctx context.Context, name string) 101 | 102 | // GetStatusCode supports the WebAssembly function export 103 | // FuncGetStatusCode. 104 | GetStatusCode(ctx context.Context) uint32 105 | 106 | // SetStatusCode supports the WebAssembly function export 107 | // FuncSetStatusCode. 108 | SetStatusCode(ctx context.Context, statusCode uint32) 109 | 110 | // GetResponseHeaderNames supports the WebAssembly function export 111 | // FuncGetHeaderNames with HeaderKindResponse. This returns nil if no 112 | // headers exist. 113 | GetResponseHeaderNames(ctx context.Context) []string 114 | 115 | // GetResponseHeaderValues supports the WebAssembly function export 116 | // FuncGetHeaderValues with HeaderKindResponse. This returns nil if no 117 | // values exist. 118 | GetResponseHeaderValues(ctx context.Context, name string) []string 119 | 120 | // SetResponseHeaderValue supports the WebAssembly function export 121 | // FuncSetHeaderValue with HeaderKindResponse. 122 | SetResponseHeaderValue(ctx context.Context, name, value string) 123 | 124 | // AddResponseHeaderValue supports the WebAssembly function export 125 | // FuncAddHeaderValue with HeaderKindResponse. 126 | AddResponseHeaderValue(ctx context.Context, name, value string) 127 | 128 | // RemoveResponseHeader supports the WebAssembly function export 129 | // FuncRemoveHeader with HeaderKindResponse. 130 | RemoveResponseHeader(ctx context.Context, name string) 131 | 132 | // ResponseBodyReader supports the WebAssembly function export 133 | // FuncReadBody with BodyKindResponse. 134 | ResponseBodyReader(ctx context.Context) io.ReadCloser 135 | 136 | // ResponseBodyWriter supports the WebAssembly function export 137 | // FuncWriteBody with BodyKindResponse. 138 | ResponseBodyWriter(ctx context.Context) io.Writer 139 | 140 | // GetResponseTrailerNames supports the WebAssembly function export 141 | // FuncGetHeaderNames with HeaderKindResponseTrailers. This returns nil if 142 | // no trailers exist or FeatureTrailers is not supported. 143 | GetResponseTrailerNames(ctx context.Context) []string 144 | 145 | // GetResponseTrailerValues supports the WebAssembly function export 146 | // FuncGetHeaderValues with HeaderKindResponseTrailers. This returns nil if 147 | // no values exist or FeatureTrailers is not supported. 148 | GetResponseTrailerValues(ctx context.Context, name string) []string 149 | 150 | // SetResponseTrailerValue supports the WebAssembly function export 151 | // FuncSetHeaderValue with HeaderKindResponseTrailers. This panics if 152 | // FeatureTrailers is not supported. 153 | SetResponseTrailerValue(ctx context.Context, name, value string) 154 | 155 | // AddResponseTrailerValue supports the WebAssembly function export 156 | // FuncAddHeaderValue with HeaderKindResponseTrailers. This panics if 157 | // FeatureTrailers is not supported. 158 | AddResponseTrailerValue(ctx context.Context, name, value string) 159 | 160 | // RemoveResponseTrailer supports the WebAssembly function export 161 | // FuncRemoveHeader with HeaderKindResponseTrailers. This panics if 162 | // FeatureTrailers is not supported. 163 | RemoveResponseTrailer(ctx context.Context, name string) 164 | 165 | // GetSourceAddr supports the WebAssembly function export FuncGetSourceAddr. 166 | GetSourceAddr(ctx context.Context) string 167 | } 168 | 169 | // eofReader is safer than reading from os.DevNull as it can never overrun 170 | // operating system file descriptors. 171 | type eofReader struct{} 172 | 173 | func (eofReader) Close() (err error) { return } 174 | func (eofReader) Read([]byte) (int, error) { return 0, io.EOF } 175 | 176 | type UnimplementedHost struct{} 177 | 178 | var _ Host = UnimplementedHost{} 179 | 180 | func (UnimplementedHost) EnableFeatures(context.Context, Features) Features { return 0 } 181 | func (UnimplementedHost) GetMethod(context.Context) string { return "GET" } 182 | func (UnimplementedHost) SetMethod(context.Context, string) {} 183 | func (UnimplementedHost) GetURI(context.Context) string { return "" } 184 | func (UnimplementedHost) SetURI(context.Context, string) {} 185 | func (UnimplementedHost) GetProtocolVersion(context.Context) string { return "HTTP/1.1" } 186 | func (UnimplementedHost) GetRequestHeaderNames(context.Context) (names []string) { return } 187 | func (UnimplementedHost) GetRequestHeaderValues(context.Context, string) (values []string) { return } 188 | func (UnimplementedHost) SetRequestHeaderValue(context.Context, string, string) {} 189 | func (UnimplementedHost) AddRequestHeaderValue(context.Context, string, string) {} 190 | func (UnimplementedHost) RemoveRequestHeader(context.Context, string) {} 191 | func (UnimplementedHost) RequestBodyReader(context.Context) io.ReadCloser { return eofReader{} } 192 | func (UnimplementedHost) RequestBodyWriter(context.Context) io.Writer { return io.Discard } 193 | func (UnimplementedHost) GetRequestTrailerNames(context.Context) (names []string) { return } 194 | func (UnimplementedHost) GetRequestTrailerValues(context.Context, string) (values []string) { return } 195 | func (UnimplementedHost) SetRequestTrailerValue(context.Context, string, string) {} 196 | func (UnimplementedHost) AddRequestTrailerValue(context.Context, string, string) {} 197 | func (UnimplementedHost) RemoveRequestTrailer(context.Context, string) {} 198 | func (UnimplementedHost) GetStatusCode(context.Context) uint32 { return 200 } 199 | func (UnimplementedHost) SetStatusCode(context.Context, uint32) {} 200 | func (UnimplementedHost) GetResponseHeaderNames(context.Context) (names []string) { return } 201 | func (UnimplementedHost) GetResponseHeaderValues(context.Context, string) (values []string) { return } 202 | func (UnimplementedHost) SetResponseHeaderValue(context.Context, string, string) {} 203 | func (UnimplementedHost) AddResponseHeaderValue(context.Context, string, string) {} 204 | func (UnimplementedHost) RemoveResponseHeader(context.Context, string) {} 205 | func (UnimplementedHost) ResponseBodyReader(context.Context) io.ReadCloser { return eofReader{} } 206 | func (UnimplementedHost) ResponseBodyWriter(context.Context) io.Writer { return io.Discard } 207 | func (UnimplementedHost) GetResponseTrailerNames(context.Context) (names []string) { return } 208 | func (UnimplementedHost) GetResponseTrailerValues(context.Context, string) (values []string) { return } 209 | func (UnimplementedHost) SetResponseTrailerValue(context.Context, string, string) {} 210 | func (UnimplementedHost) AddResponseTrailerValue(context.Context, string, string) {} 211 | func (UnimplementedHost) RemoveResponseTrailer(context.Context, string) {} 212 | func (UnimplementedHost) GetSourceAddr(context.Context) string { return "1.1.1.1:12345" } 213 | -------------------------------------------------------------------------------- /api/handler/wasm.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | // CtxNext is the result of FuncHandleRequest. For compatability with 4 | // WebAssembly Core Specification 1.0, two uint32 values are combined into a 5 | // single uint64 in the following order: 6 | // 7 | // - ctx: opaque 32-bits the guest defines and the host propagates to 8 | // FuncHandleResponse. A typical use is correlation of request state. 9 | // - next: one to proceed to the next handler on the host. zero to skip any 10 | // next handler. Guests skip when they wrote a response or decided not to. 11 | // 12 | // When the guest decides to proceed to the next handler, it can return 13 | // `ctxNext=1` which is the same as `next=1` without any request context. If it 14 | // wants the host to propagate request context, it shifts that into the upper 15 | // 32-bits of ctxNext like so: 16 | // 17 | // ctxNext = uint64(reqCtx) << 32 18 | // if next { 19 | // ctxNext |= 1 20 | // } 21 | // 22 | // # Examples 23 | // 24 | // - 0<<32|0 (0): don't proceed to the next handler. 25 | // - 0<<32|1 (1): proceed to the next handler without context state. 26 | // - 16<<32|1 (68719476737): proceed to the next handler and call 27 | // FuncHandleResponse with 16. 28 | // - 16<<32|16 (68719476736): the value 16 is ignored because 29 | // FuncHandleResponse won't be called. 30 | type CtxNext uint64 31 | 32 | // BufLimit is the possibly zero maximum length of a result value to write in 33 | // bytes. If the actual value is larger than this, nothing is written to 34 | // memory. 35 | type BufLimit = uint32 36 | 37 | // CountLen describes a possible empty sequence of NUL-terminated strings. For 38 | // compatability with WebAssembly Core Specification 1.0, two uint32 values are 39 | // combined into a single uint64 in the following order: 40 | // 41 | // - count: zero if the sequence is empty, or the count of strings. 42 | // - len: possibly zero length of the sequence, including NUL-terminators. 43 | // 44 | // If the uint64 result is zero, the sequence is empty. Otherwise, you need to 45 | // split the results like so. 46 | // 47 | // - count: `uint32(countLen >> 32)` 48 | // - len: `uint32(countLen)` 49 | // 50 | // # Examples 51 | // 52 | // - "": 0<<32|0 or simply zero. 53 | // - "Accept\0": 1<<32|7 54 | // - "Content-Type\0Content-Length\0": 2<<32|28 55 | type CountLen = uint64 56 | 57 | // EOFLen is the result of FuncReadBody which allows callers to know if the 58 | // bytes returned are the end of the stream. For compatability with WebAssembly 59 | // Core Specification 1.0, two uint32 values are combined into a single uint64 60 | // in the following order: 61 | // 62 | // - eof: the body is exhausted. 63 | // - len: possibly zero length of bytes read from the body. 64 | // 65 | // Here's how to split the results: 66 | // 67 | // - eof: `uint32(eofLen >> 32)` 68 | // - len: `uint32(eofLen)` 69 | // 70 | // # Examples 71 | // 72 | // - 1<<32|0 (4294967296): EOF and no bytes were read 73 | // - 0<<32|16 (16): 16 bytes were read and there may be more available. 74 | // 75 | // Note: `EOF` is not an error, so process `len` bytes returned regardless. 76 | type EOFLen = uint64 77 | 78 | type BodyKind uint32 79 | 80 | const ( 81 | // BodyKindRequest represents an operation on an HTTP request body. 82 | // 83 | // # Notes on FuncReadBody 84 | // 85 | // FeatureBufferResponse is required to read the request body without 86 | // consuming it. To enable it, call FuncEnableFeatures before FuncNext. 87 | // Otherwise, a downstream handler may panic attempting to read a request 88 | // body already read upstream. 89 | // 90 | // # Notes on FuncWriteBody 91 | // 92 | // The first call to FuncWriteBody in FuncHandleRequest overwrites any request 93 | // body. 94 | BodyKindRequest BodyKind = 0 95 | 96 | // BodyKindResponse represents an operation on an HTTP request body. 97 | // 98 | // # Notes on FuncReadBody 99 | // 100 | // FeatureBufferResponse is required to read the response body produced by 101 | // FuncNext. To enable it, call FuncEnableFeatures beforehand. Otherwise, 102 | // a handler may panic calling FuncReadBody with BodyKindResponse. 103 | // 104 | // # Notes on FuncWriteBody 105 | // 106 | // The first call to FuncWriteBody in FuncHandleRequest or after FuncNext 107 | // overwrites any response body. 108 | BodyKindResponse BodyKind = 1 109 | ) 110 | 111 | type HeaderKind uint32 112 | 113 | const ( 114 | // HeaderKindRequest represents an operation on HTTP request headers. 115 | HeaderKindRequest HeaderKind = 0 116 | 117 | // HeaderKindResponse represents an operation on HTTP response headers. 118 | HeaderKindResponse HeaderKind = 1 119 | 120 | // HeaderKindRequestTrailers represents an operation on HTTP request 121 | // trailers (trailing headers). This requires FeatureTrailers. 122 | // 123 | // To enable FeatureTrailers, call FuncEnableFeatures prior to FuncNext. 124 | // Doing otherwise, may result in a panic. 125 | HeaderKindRequestTrailers HeaderKind = 2 126 | 127 | // HeaderKindResponseTrailers represents an operation on HTTP response 128 | // trailers (trailing headers). This requires FeatureTrailers. 129 | // 130 | // To enable FeatureTrailers, call FuncEnableFeatures prior to FuncNext. 131 | // Doing otherwise, may result in a panic. 132 | HeaderKindResponseTrailers HeaderKind = 3 133 | ) 134 | 135 | const ( 136 | // HostModule is the WebAssembly module name of the ABI this middleware 137 | // implements. 138 | // 139 | // Note: This is lower-hyphen case while functions are lower_underscore to 140 | // follow conventions of wit-bindgen, which retains the case format of the 141 | // interface filename as the module name, but converts function and 142 | // parameter names to lower_underscore format. 143 | // 144 | // See https://github.com/http-wasm/http-wasm-abi/blob/main/http_handler/http_handler.wit.md 145 | HostModule = "http_handler" 146 | 147 | // FuncEnableFeatures tries to enable the given features and returns the 148 | // Features bitflag supported by the host. To have any affect, this must be 149 | // called prior to returning from FuncHandleRequest. 150 | // 151 | // This may be called prior to FuncHandleRequest, for example inside a 152 | // start function. Doing so reduces overhead per-call and also allows the 153 | // guest to fail early on unsupported. 154 | // 155 | // If called during FuncHandleRequest, any new features are enabled for the 156 | // scope of the current request. This allows fine-grained access to 157 | // expensive features such as FeatureBufferResponse. 158 | // 159 | // TODO: document on http-wasm-abi 160 | FuncEnableFeatures = "enable_features" 161 | 162 | // FuncGetConfig writes configuration from the host to memory if it exists 163 | // and isn't larger than BufLimit. The result is its length in bytes. 164 | // 165 | // Note: Configuration is determined by the guest and is not necessarily 166 | // UTF-8 encoded. 167 | // 168 | // TODO: document on http-wasm-abi 169 | FuncGetConfig = "get_config" 170 | 171 | // FuncLogEnabled returns 1 if the api.LogLevel is enabled. This value may 172 | // be cached at request granularity. 173 | // 174 | // This function is used to avoid unnecessary overhead generating log 175 | // messages that the host would discard due to its level being below this. 176 | // 177 | // TODO: document on http-wasm-abi 178 | FuncLogEnabled = "log_enabled" 179 | 180 | // FuncLog logs a message to the host's logs at the given api.LogLevel. 181 | // 182 | // See https://github.com/http-wasm/http-wasm-abi/blob/main/http_handler/http_handler.wit.md#log 183 | FuncLog = "log" 184 | 185 | // FuncHandleRequest is the entrypoint guest export called by the host when 186 | // processing a request. 187 | // 188 | // To proceed to the next handler, the guest returns CtxNext `next=1`. The 189 | // simplest result is uint64(1). Guests who need request correlation can 190 | // also set the CtxNext "ctx" field. This will be propagated by the host as 191 | // the `reqCtx` parameter of FuncHandleResponse. 192 | // 193 | // To skip any next handler, the guest returns CtxNext `next=0`, or simply 194 | // uint64(0). In this case, FuncHandleResponse will not be called. 195 | FuncHandleRequest = "handle_request" 196 | 197 | // FuncHandleResponse is called by the host after processing the next 198 | // handler when the guest returns CtxNext `next=1` from FuncHandleRequest. 199 | // 200 | // The `reqCtx` parameter is a possibly zero CtxNext "ctx" field the host 201 | // the host propagated from FuncHandleRequest. This allows request 202 | // correlation for guests who need it. 203 | // 204 | // The `isError` parameter is one if there was a host error producing a 205 | // response. This allows guests to clean up any resources. 206 | // 207 | // By default, whether the next handler flushes the response prior to 208 | // returning is implementation-specific. If your handler needs to inspect 209 | // or manipulate the downstream response, enable FeatureBufferResponse via 210 | // FuncEnableFeatures prior to returning from FuncHandleRequest. 211 | // TODO: update 212 | FuncHandleResponse = "handle_response" 213 | 214 | // FuncGetMethod writes the method to memory if it isn't larger than 215 | // BufLimit. The result is its length in bytes. Ex. "GET" 216 | // 217 | // TODO: document on http-wasm-abi 218 | FuncGetMethod = "get_method" 219 | 220 | // FuncSetMethod overwrites the method with one read from memory. 221 | // 222 | // TODO: document on http-wasm-abi 223 | FuncSetMethod = "set_method" 224 | 225 | // FuncGetURI writes the URI to memory if it isn't larger than BufLimit. 226 | // The result is its length in bytes. Ex. "/v1.0/hi?name=panda" 227 | // 228 | // Note: The URI may include query parameters. 229 | // 230 | // TODO: update document on http-wasm-abi 231 | // See https://github.com/http-wasm/http-wasm-abi/blob/main/http_handler/http_handler.wit.md#get_uri 232 | FuncGetURI = "get_uri" 233 | 234 | // FuncSetURI overwrites the URI with one read from memory. 235 | // 236 | // Note: The URI may include query parameters. 237 | // 238 | // TODO: update document on http-wasm-abi 239 | // See https://github.com/http-wasm/http-wasm-abi/blob/main/http_handler/http_handler.wit.md#set_uri 240 | FuncSetURI = "set_uri" 241 | 242 | // FuncGetProtocolVersion writes the HTTP protocol version to memory if it 243 | // isn't larger than BufLimit. The result is its length in bytes. 244 | // Ex. "HTTP/1.1" 245 | // 246 | // See https://www.rfc-editor.org/rfc/rfc9110#name-protocol-version 247 | // TODO: document on http-wasm-abi 248 | FuncGetProtocolVersion = "get_protocol_version" 249 | 250 | // FuncGetHeaderNames writes all names for the given HeaderKind, 251 | // NUL-terminated, to memory if the encoded length isn't larger than 252 | // BufLimit. CountLen is returned regardless of whether memory was written. 253 | // 254 | // TODO: document on http-wasm-abi 255 | FuncGetHeaderNames = "get_header_names" 256 | 257 | // FuncGetHeaderValues writes all values of the given HeaderKind and name, 258 | // NUL-terminated, to memory if the encoded length isn't larger than 259 | // BufLimit. CountLen is returned regardless of whether memory was written. 260 | // 261 | // TODO: document on http-wasm-abi 262 | FuncGetHeaderValues = "get_header_values" 263 | 264 | // FuncSetHeaderValue overwrites all values of the given HeaderKind and name 265 | // with the input. 266 | // 267 | // TODO: document on http-wasm-abi 268 | FuncSetHeaderValue = "set_header_value" 269 | 270 | // FuncAddHeaderValue adds a single value for the given HeaderKind and 271 | // name. 272 | // 273 | // TODO: document on http-wasm-abi 274 | FuncAddHeaderValue = "add_header_value" 275 | 276 | // FuncRemoveHeader removes any values for the given HeaderKind and name. 277 | // 278 | // TODO: document on http-wasm-abi 279 | FuncRemoveHeader = "remove_header" 280 | 281 | // FuncReadBody reads up to BufLimit bytes remaining in the BodyKind body 282 | // into memory at offset `buf`. A zero BufLimit will panic. 283 | // 284 | // The result is EOFLen, indicating the count of bytes read and whether 285 | // there may be more available. 286 | // 287 | // Unlike `get_XXX` functions, this function is stateful, so repeated calls 288 | // reads what's remaining in the stream, as opposed to starting from zero. 289 | // Callers do not have to exhaust the stream until `EOF`. 290 | // 291 | // TODO: document on http-wasm-abi 292 | FuncReadBody = "read_body" 293 | 294 | // FuncWriteBody reads `buf_len` bytes at memory offset `buf` and writes 295 | // them to the pending BodyKind body. 296 | // 297 | // Unlike `set_XXX` functions, this function is stateful, so repeated calls 298 | // write to the current stream. 299 | // 300 | // TODO: document on http-wasm-abi 301 | FuncWriteBody = "write_body" 302 | 303 | // FuncGetStatusCode returns the status code produced by FuncNext. This 304 | // requires FeatureBufferResponse. 305 | // 306 | // To enable FeatureBufferResponse, FuncEnableFeatures prior to FuncNext. 307 | // Doing otherwise, or calling before FuncNext will panic. 308 | // 309 | // TODO: document on http-wasm-abi 310 | FuncGetStatusCode = "get_status_code" 311 | 312 | // FuncSetStatusCode overrides the status code. The default is 200. 313 | // 314 | // To use this function after FuncNext, set FeatureBufferResponse via 315 | // FuncEnableFeatures. Otherwise, this can be called when FuncNext wasn't. 316 | // 317 | // TODO: document on http-wasm-abi 318 | FuncSetStatusCode = "set_status_code" 319 | 320 | // FuncGetSourceAddr writes the SourceAddr to memory if it isn't larger than BufLimit. 321 | // The result is its length in bytes. Ex. "1.1.1.1:12345" or "[fe80::101e:2bdf:8bfb:b97e]:12345" 322 | // 323 | // TODO: document on http-wasm-abi 324 | FuncGetSourceAddr = "get_source_addr" 325 | ) 326 | -------------------------------------------------------------------------------- /api/wasm.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | const Memory = "memory" 4 | -------------------------------------------------------------------------------- /examples/auth.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/examples/auth.wasm -------------------------------------------------------------------------------- /examples/auth.wat: -------------------------------------------------------------------------------- 1 | ;; This example module is written in WebAssembly Text Format to show the 2 | ;; how a handler works and that it is decoupled from other ABI such as WASI. 3 | ;; Most users will prefer a higher-level language such as C, Rust or TinyGo. 4 | (module $auth 5 | 6 | ;; get_header_values writes all header names of the given $kind and $name, 7 | ;; NUL-terminated, to memory if the encoded length isn't larger than 8 | ;; $buf_limit. The result is regardless of whether memory was written. 9 | (import "http_handler" "get_header_values" (func $get_header_values 10 | (param $kind i32) 11 | (param $name i32) (param $name_len i32) 12 | (param $buf i32) (param $buf_limit i32) 13 | (result (; count << 32| len ;) i64))) 14 | 15 | ;; set_header_value overwrites a header of the given $kind and $name with a 16 | ;; single value. 17 | (import "http_handler" "set_header_value" (func $set_header_value 18 | (param $kind i32) 19 | (param $name i32) (param $name_len i32) 20 | (param $value i32) (param $value_len i32))) 21 | 22 | ;; set_status_code overrides the status code. The default is 200. 23 | (import "http_handler" "set_status_code" (func $set_status_code 24 | (param $status_code i32))) 25 | 26 | ;; http_handler guests are required to export "memory", so that imported 27 | ;; functions like "get_header" can read memory. 28 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 29 | 30 | ;; buf is an arbitrary area to write data. 31 | (global $buf i32 (i32.const 1024)) 32 | 33 | (global $authorization_name i32 (i32.const 0)) 34 | (data (i32.const 0) "Authorization") 35 | (global $authorization_name_len i32 (i32.const 13)) 36 | 37 | ;; We expect the username "Aladdin" and password "open sesame". 38 | (global $authorization_values i32 (i32.const 64)) 39 | (data (i32.const 64) "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==\00") 40 | (global $authorization_values_len i32 (i32.const 35)) 41 | 42 | ;; get_authorization reads the Authorization header into memory 43 | (func $get_authorization (result (; $count_length ;) i64) 44 | (call $get_header_values 45 | (i32.const 0) ;; header_kind_request 46 | (global.get $authorization_name) 47 | (global.get $authorization_name_len) 48 | (global.get $buf) 49 | (global.get $authorization_values_len))) 50 | 51 | (global $authenticate_name i32 (i32.const 128)) 52 | (data (i32.const 128) "WWW-Authenticate") 53 | (global $authenticate_name_len i32 (i32.const 16)) 54 | 55 | (global $authenticate_value i32 (i32.const 196)) 56 | (data (i32.const 196) "Basic realm=\"test\"") 57 | (global $authenticate_value_len i32 (i32.const 18)) 58 | 59 | ;; set_authenticate adds the WWW-Authenticate header 60 | (func $set_authenticate 61 | (call $set_header_value 62 | (i32.const 1) ;; header_kind_response 63 | (global.get $authenticate_name) 64 | (global.get $authenticate_name_len) 65 | (global.get $authenticate_value) 66 | (global.get $authenticate_value_len))) 67 | 68 | ;; handle_request tries BASIC authentication and dispatches to the next 69 | ;; handler or returns 401. 70 | (func (export "handle_request") (result (; ctx_next ;) i64) 71 | 72 | (local $result i64) 73 | (local $count i32) 74 | (local $len i32) 75 | (local $authorization_eq i32) 76 | (local $ctx_next i64) 77 | 78 | (local.set $result (call $get_authorization)) 79 | 80 | ;; count = uint32(result >> 32) 81 | (local.set $count 82 | (i32.wrap_i64 (i64.shr_u (local.get $result) (i64.const 32)))) 83 | 84 | (if (i32.ne (local.get $count) (i32.const 1)) 85 | (then ;; multiple headers, invalid 86 | (call $set_authenticate) 87 | (call $set_status_code (i32.const 401)) 88 | (return (i64.const 0)))) ;; don't call the next handler 89 | 90 | ;; len = uint32(result) 91 | (local.set $len (i32.wrap_i64 (local.get $result))) 92 | 93 | (if (i32.ne (global.get $authorization_values_len) (local.get $len)) 94 | (then ;; authorization_values_length != i32($header_value) 95 | (call $set_status_code (i32.const 401)) 96 | (return (i64.const 0)))) ;; don't call the next handler 97 | 98 | (local.set $authorization_eq (call $memeq 99 | (global.get $buf) 100 | (global.get $authorization_values) 101 | (global.get $authorization_values_len))) 102 | 103 | (if (i32.eqz (local.get $authorization_eq)) 104 | (then ;; authenticate_value != authorization_values 105 | (call $set_status_code (i32.const 401))) 106 | (else ;; authorization passed! 107 | (local.set $ctx_next (i64.const 1)))) ;; call the next handler 108 | (return (local.get $ctx_next)) 109 | 110 | ;; uint32(ctx_next) == 1 means proceed to the next handler on the host. 111 | (return (i64.const 1))) 112 | 113 | ;; handle_response is no-op as this is a request-only handler. 114 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 115 | 116 | ;; memeq is like memcmp except it returns 0 (ne) or 1 (eq) 117 | (func $memeq (param $ptr1 i32) (param $ptr2 i32) (param $len i32) (result i32) 118 | (local $i1 i32) 119 | (local $i2 i32) 120 | (local.set $i1 (local.get $ptr1)) ;; i1 := ptr1 121 | (local.set $i2 (local.get $ptr2)) ;; i2 := ptr1 122 | 123 | (loop $len_gt_zero 124 | ;; if mem[i1] != mem[i2] 125 | (if (i32.ne (i32.load8_u (local.get $i1)) (i32.load8_u (local.get $i2))) 126 | (then (return (i32.const 0)))) ;; return 0 127 | 128 | (local.set $i1 (i32.add (local.get $i1) (i32.const 1))) ;; i1++ 129 | (local.set $i2 (i32.add (local.get $i2) (i32.const 1))) ;; i2++ 130 | (local.set $len (i32.sub (local.get $len) (i32.const 1))) ;; $len-- 131 | 132 | ;; if $len > 0 { continue } else { break } 133 | (br_if $len_gt_zero (i32.gt_u (local.get $len) (i32.const 0)))) 134 | 135 | (i32.const 1)) ;; return 1 136 | ) 137 | -------------------------------------------------------------------------------- /examples/config.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/examples/config.wasm -------------------------------------------------------------------------------- /examples/config.wat: -------------------------------------------------------------------------------- 1 | ;; This example module is written in WebAssembly Text Format to show the 2 | ;; how a handler works and that it is decoupled from other ABI such as WASI. 3 | ;; Most users will prefer a higher-level language such as C, Rust or TinyGo. 4 | (module $config 5 | ;; enable_features tries to enable the given features and returns the entire 6 | ;; feature bitflag supported by the host. 7 | (import "http_handler" "enable_features" (func $enable_features 8 | (param $enable_features i32) 9 | (result (; enabled_features ;) i32))) 10 | 11 | ;; get_config writes configuration from the host to memory if it exists and 12 | ;; isn't larger than $buf_limit. The result is its length in bytes. 13 | (import "http_handler" "get_config" (func $get_config 14 | (param $buf i32) (param $buf_limit i32) 15 | (result (; len ;) i32))) 16 | 17 | ;; handle_request just calls next by returning non-zero. 18 | (func (export "handle_request") (result (; ctx_next ;) i64) 19 | ;; uint32(ctx_next) == 1 means proceed to the next handler on the host. 20 | (return (i64.const 1))) 21 | 22 | ;; handle_response is no-op as this is a request-only handler. 23 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 24 | 25 | ;; http_handler guests are required to export "memory", so that imported 26 | ;; functions like "get_header" can read memory. 27 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 28 | 29 | (func $must_enable_features 30 | (local $config_len i32) 31 | (local $required_features i32) 32 | (local $enabled_features i32) 33 | 34 | (local.set $config_len 35 | (call $get_config (i32.const 0) (i32.const 8))) 36 | 37 | ;; if config_len != size_of_uint64le { panic } 38 | (if (i32.ne (local.get $config_len) (i32.const 8)) 39 | (then unreachable)) 40 | 41 | (local.set $required_features (i32.load (i32.const 0))) 42 | 43 | ;; enabled_features := enable_features(required_features) 44 | (local.set $enabled_features 45 | (call $enable_features (local.get $required_features))) 46 | 47 | ;; if required_features == 0 48 | (if (i32.eqz (local.get $required_features)) 49 | ;; if enabled_features != 0 { panic } 50 | (then (if (i32.ne 51 | (local.get $enabled_features) 52 | (i32.const 0)) 53 | (then unreachable))) 54 | ;; else if enabled_features&required_features == 0 { panic } 55 | (else (if (i32.eqz (i32.and 56 | (local.get $enabled_features) 57 | (local.get $required_features))) 58 | (then unreachable))))) 59 | 60 | (start $must_enable_features) 61 | ) 62 | -------------------------------------------------------------------------------- /examples/log.wasm: -------------------------------------------------------------------------------- 1 | asm```~`/ http_handler log_enabled http_handlerlog A A -memoryhandle_requesthandle_response 2 | AAF@ A##B  A hello worldtnamelog log_enabledlog9levellevelbuf buf_limitreqCtxis_errormessage message_len -------------------------------------------------------------------------------- /examples/log.wat: -------------------------------------------------------------------------------- 1 | ;; This example module is written in WebAssembly Text Format to show the 2 | ;; how a handler works and that it is decoupled from other ABI such as WASI. 3 | ;; Most users will prefer a higher-level language such as C, Rust or TinyGo. 4 | (module $log 5 | 6 | ;; log_enabled returns 1 if the $level is enabled. This value may be cached 7 | ;; at request granularity. 8 | (import "http_handler" "log_enabled" (func $log_enabled 9 | (param $level i32) 10 | (result (; 0 or enabled(1) ;) i32))) 11 | 12 | ;; logs a message to the host's logs at the given $level. 13 | (import "http_handler" "log" (func $log 14 | (param $level i32) 15 | (param $buf i32) (param $buf_limit i32))) 16 | 17 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 18 | (global $message i32 (i32.const 0)) 19 | (data (i32.const 0) "hello world") 20 | (global $message_len i32 (i32.const 11)) 21 | 22 | (func (export "handle_request") (result (; ctx_next ;) i64) 23 | ;; We expect debug logging to be disabled. Panic otherwise! 24 | (if (i32.eq 25 | (call $log_enabled (i32.const -1)) ;; log_level_debug 26 | (i32.const 1)) ;; true 27 | (then unreachable)) 28 | 29 | (call $log 30 | (i32.const 0) ;; log_level_info 31 | (global.get $message) 32 | (global.get $message_len)) 33 | 34 | ;; uint32(ctx_next) == 1 means proceed to the next handler on the host. 35 | (return (i64.const 1))) 36 | 37 | ;; handle_response is no-op as this is a request-only handler. 38 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 39 | ) 40 | -------------------------------------------------------------------------------- /examples/redact.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/examples/redact.wasm -------------------------------------------------------------------------------- /examples/redact.wat: -------------------------------------------------------------------------------- 1 | ;; This example module is written in WebAssembly Text Format to show the 2 | ;; how a handler works and that it is decoupled from other ABI such as WASI. 3 | ;; Most users will prefer a higher-level language such as C, Rust or TinyGo. 4 | (module $redact 5 | ;; enable_features tries to enable the given features and returns the entire 6 | ;; feature bitflag supported by the host. 7 | (import "http_handler" "enable_features" (func $enable_features 8 | (param $enable_features i32) 9 | (result (; enabled_features ;) i32))) 10 | 11 | ;; get_config writes configuration from the host to memory if it exists and 12 | ;; isn't larger than $buf_limit. The result is its length in bytes. 13 | (import "http_handler" "get_config" (func $get_config 14 | (param $buf i32) (param $buf_limit i32) 15 | (result (; len ;) i32))) 16 | 17 | ;; read_body reads up to $buf_limit bytes remaining in the $kind body into 18 | ;; memory at offset $buf. A zero $buf_limit will panic. 19 | ;; 20 | ;; The result is `0 or EOF(1) << 32|len`, where `len` is the length in bytes 21 | ;; read. 22 | (import "http_handler" "read_body" (func $read_body 23 | (param $kind i32) 24 | (param $buf i32) (param $buf_len i32) 25 | (result (; 0 or EOF(1) << 32 | len ;) i64))) 26 | 27 | ;; write_body reads $buf_len bytes at memory offset `buf` and writes them to 28 | ;; the pending $kind body. 29 | (import "http_handler" "write_body" (func $write_body 30 | (param $kind i32) 31 | (param $buf i32) (param $buf_len i32))) 32 | 33 | ;; http_handler guests are required to export "memory", so that imported 34 | ;; functions like $read_body can read memory. 35 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 36 | 37 | ;; eof is the upper 32-bits of the $read_body result on EOF. 38 | (global $eof i64 (i64.const 4294967296)) ;; `1<<32|0` 39 | 40 | ;; body is the memory offset past any initialization data. 41 | (global $body i32 (i32.const 1024)) 42 | 43 | (global $secret i32 (i32.const 0)) 44 | ;; $secret_len is mutable as it is initialized during start. 45 | (global $secret_len (mut i32) (i32.const 0)) 46 | 47 | ;; read_secret ensures there's a non-zero length secret configured. 48 | (func $read_secret 49 | (local $config_len i32) 50 | 51 | (local.set $config_len 52 | (call $get_config (global.get $secret) (global.get $body))) 53 | 54 | ;; if config_len > body { panic } 55 | (if (i32.gt_u (local.get $config_len) (global.get $body)) 56 | (then unreachable)) 57 | 58 | ;; secret_len = config_len 59 | (global.set $secret_len (local.get $config_len)) 60 | 61 | ;; if secret_len == 0 { panic } 62 | (if (i32.eqz (global.get $secret_len)) 63 | (then unreachable))) 64 | 65 | ;; required_features := feature_buffer_request|feature_buffer_response 66 | (global $required_features i32 (i32.const 3)) 67 | 68 | ;; enable_buffering ensures we can inspect request and response bodies 69 | ;; without interfering with the next handler. 70 | (func $enable_buffering 71 | (local $enabled_features i32) 72 | 73 | ;; enabled_features := enable_features(required_features) 74 | (local.set $enabled_features 75 | (call $enable_features (global.get $required_features))) 76 | 77 | ;; if enabled_features&required_features == 0 { panic } 78 | (if (i32.eqz (i32.and 79 | (local.get $enabled_features) 80 | (global.get $required_features))) 81 | (then unreachable))) 82 | 83 | (start $main) 84 | (func $main 85 | (call $enable_buffering) 86 | (call $read_secret)) 87 | 88 | ;; must_read_body reads and returns the length of the body of the given $kind 89 | ;; or fails if out of memory. 90 | (func $must_read_body (param $kind i32) (result (; len ;) i32) 91 | (local $limit i32) 92 | (local $result i64) 93 | (local $len i32) 94 | 95 | ;; set limit to the amount of available memory without growing. 96 | (local.set $limit (i32.sub 97 | (i32.mul (memory.size) (i32.const 65536)) 98 | (global.get $body))) 99 | 100 | ;; result = read_body(kind, body, limit) 101 | (local.set $result 102 | (call $read_body 103 | (local.get $kind) 104 | (global.get $body) (local.get $limit))) 105 | 106 | ;; len = uint32(result) 107 | (local.set $len (i32.wrap_i64 (local.get $result))) 108 | 109 | ;; if result & eof != eof { panic } 110 | (if (i64.ne 111 | (i64.and (local.get $result) (global.get $eof)) 112 | (global.get $eof)) 113 | (then unreachable)) ;; fail as we couldn't buffer the whole response. 114 | 115 | (local.get $len)) 116 | 117 | ;; handle_request redacts any request body. 118 | (func (export "handle_request") (result (; ctx_next ;) i64) 119 | (local $len i32) 120 | 121 | ;; load the request body from the upstream handler into memory. 122 | (local.set $len (call $must_read_body (i32.const 0))) 123 | 124 | ;; if redaction affected the copy of the request in memory... 125 | (if (call $redact (global.get $body) (local.get $len)) 126 | (then ;; overwrite the request body on the host with the redacted one. 127 | (call $write_body 128 | (i32.const 0) ;; body_kind_request 129 | (global.get $body) (local.get $len)))) 130 | 131 | ;; uint32(ctx_next) == 1 means proceed to the next handler on the host. 132 | (return (i64.const 1))) 133 | 134 | ;; handle_response redacts any request body. 135 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 136 | (local $len i32) 137 | 138 | (if (i32.eq (local.get $is_error) (i32.const 1)) 139 | (then (return))) ;; nothing to redact on error 140 | 141 | ;; load the response body from the downstream handler into memory. 142 | (local.set $len (call $must_read_body (i32.const 1))) 143 | 144 | ;; if redaction affected the copy of the response in memory... 145 | (if (call $redact (global.get $body) (local.get $len)) 146 | (then ;; overwrite the response body on the host with the redacted one. 147 | (call $write_body 148 | (i32.const 1) ;; body_kind_response 149 | (global.get $body) (local.get $len))))) 150 | 151 | ;; redact inline replaces any secrets in the memory region with hashes (#). 152 | (func $redact (param $ptr i32) (param $len i32) (result (; redacted ;) i32) 153 | (local $redacted i32) 154 | 155 | (if (i32.eqz (call $can_redact (local.get $len))) 156 | (then (return (i32.const 0)))) ;; can't redact 157 | 158 | (loop $redacting 159 | ;; if mem[i:secret_len] == secret 160 | (if (call $memeq (local.get $ptr) (global.get $secret) (global.get $secret_len)) 161 | (then ;; redact by overwriting the region with hashes (#) 162 | (local.set $redacted (i32.const 1)) 163 | (memory.fill 164 | (local.get $ptr) 165 | (i32.const 35) ;; # in ASCII 166 | (global.get $secret_len)))) 167 | 168 | (local.set $ptr (i32.add (local.get $ptr) (i32.const 1))) ;; ptr++ 169 | (local.set $len (i32.sub (local.get $len) (i32.const 1))) ;; $len-- 170 | 171 | ;; if can_redact(len) { continue } else { break } 172 | (br_if $redacting (call $can_redact (local.get $len)))) 173 | 174 | ;; return whether the memory changed due to redaction 175 | (local.get $redacted)) 176 | 177 | ;; can_redact ensures the current pointer can be compared to the secret. 178 | (func $can_redact (param $len i32) (result (; ok ;) i32) 179 | (i32.and 180 | (i32.gt_u (global.get $secret_len) (local.get $len) 181 | (i32.gt_u (local.get $len) (i32.const 0))))) 182 | 183 | ;; memeq is like memcmp except it returns 0 (ne) or 1 (eq) 184 | (func $memeq (param $ptr1 i32) (param $ptr2 i32) (param $len i32) (result i32) 185 | (local $i1 i32) 186 | (local $i2 i32) 187 | (local.set $i1 (local.get $ptr1)) ;; i1 := ptr1 188 | (local.set $i2 (local.get $ptr2)) ;; i2 := ptr1 189 | 190 | (loop $len_gt_zero 191 | ;; if mem[i1] != mem[i2] 192 | (if (i32.ne (i32.load8_u (local.get $i1)) (i32.load8_u (local.get $i2))) 193 | (then (return (i32.const 0)))) ;; return 0 194 | 195 | (local.set $i1 (i32.add (local.get $i1) (i32.const 1))) ;; i1++ 196 | (local.set $i2 (i32.add (local.get $i2) (i32.const 1))) ;; i2++ 197 | (local.set $len (i32.sub (local.get $len) (i32.const 1))) ;; $len-- 198 | 199 | ;; if $len > 0 { continue } else { break } 200 | (br_if $len_gt_zero (i32.gt_u (local.get $len) (i32.const 0)))) 201 | 202 | (i32.const 1)) ;; return 1 203 | ) 204 | -------------------------------------------------------------------------------- /examples/router.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/examples/router.wasm -------------------------------------------------------------------------------- /examples/router.wat: -------------------------------------------------------------------------------- 1 | ;; This example module is written in WebAssembly Text Format to show the 2 | ;; how a handler works and that it is decoupled from other ABI such as WASI. 3 | ;; Most users will prefer a higher-level language such as C, Rust or TinyGo. 4 | (module $router 5 | ;; get_uri writes the URI to memory if it isn't larger than $buf_limit. The 6 | ;; result is its length in bytes. 7 | (import "http_handler" "get_uri" (func $get_uri 8 | (param $buf i32) (param $buf_limit i32) 9 | (result (; len ;) i32))) 10 | 11 | ;; set_uri overwrites the URI with one read from memory. 12 | (import "http_handler" "set_uri" (func $set_uri 13 | (param $uri i32) (param $uri_len i32))) 14 | 15 | ;; set_header_value overwrites a header of the given $kind and $name with a 16 | ;; single value. 17 | (import "http_handler" "set_header_value" (func $set_header_value 18 | (param $kind i32) 19 | (param $name i32) (param $name_len i32) 20 | (param $value i32) (param $value_len i32))) 21 | 22 | ;; write_body reads $buf_len bytes at memory offset `buf` and writes them to 23 | ;; the pending $kind body. 24 | (import "http_handler" "write_body" (func $write_body 25 | (param $kind i32) 26 | (param $buf i32) (param $buf_len i32))) 27 | 28 | ;; http_handler guests are required to export "memory", so that imported 29 | ;; functions like "log" can read memory. 30 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 31 | 32 | ;; uri is an arbitrary area to write data. 33 | (global $uri i32 (i32.const 1024)) 34 | (global $uri_limit i32 (i32.const 256)) 35 | 36 | (global $path_prefix i32 (i32.const 0)) 37 | (data (i32.const 0) "/host") 38 | (global $path_prefix_len i32 (i32.const 5)) 39 | 40 | (global $content_type_name i32 (i32.const 32)) 41 | (data (i32.const 32) "Content-Type") 42 | (global $content_type_name_len i32 (i32.const 12)) 43 | 44 | (global $content_type_value i32 (i32.const 64)) 45 | (data (i32.const 64) "text/plain") 46 | (global $content_type_value_len i32 (i32.const 10)) 47 | 48 | (global $body i32 (i32.const 96)) 49 | (data (i32.const 96) "hello world") 50 | (global $body_len i32 (i32.const 11)) 51 | 52 | ;; handle_request implements a simple HTTP router. 53 | (func (export "handle_request") (result (; ctx_next ;) i64) 54 | 55 | (local $uri_len i32) 56 | 57 | ;; First, read the uri into memory if not larger than our limit. 58 | 59 | ;; uri_len = get_uri(uri, uri_limit) 60 | (local.set $uri_len 61 | (call $get_uri (global.get $uri) (global.get $uri_limit))) 62 | 63 | ;; if uri_len > uri_limit { next() } 64 | (if (i32.gt_u (local.get $uri_len) (global.get $uri_limit)) 65 | (then 66 | (return (i64.const 1)))) ;; dispatch if the uri is too long. 67 | 68 | ;; Next, strip any paths starting with '/host' and dispatch. 69 | 70 | ;; if path_prefix_len <= uri_len 71 | (if (i32.eqz (i32.gt_u (global.get $path_prefix_len) (local.get $uri_len))) 72 | (then 73 | 74 | (if (call $memeq ;; uri[0:path_prefix_len] == path_prefix 75 | (global.get $uri) 76 | (global.get $path_prefix) 77 | (global.get $path_prefix_len)) 78 | (then 79 | (call $set_uri ;; uri = uri[path_prefix_len:] 80 | (i32.add (global.get $uri) (global.get $path_prefix_len)) 81 | (i32.sub (local.get $uri_len) (global.get $path_prefix_len))) 82 | (return (i64.const 1)))))) ;; dispatch with the stripped path. 83 | 84 | ;; Otherwise, serve a static response. 85 | (call $set_header_value 86 | (i32.const 1) ;; header_kind_response 87 | (global.get $content_type_name) 88 | (global.get $content_type_name_len) 89 | (global.get $content_type_value) 90 | (global.get $content_type_value_len)) 91 | (call $write_body 92 | (i32.const 1) ;; body_kind_response 93 | (global.get $body) 94 | (global.get $body_len)) 95 | (return (i64.const 0))) ;; don't call the next handler 96 | 97 | ;; handle_response is no-op as this is a request-only handler. 98 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 99 | 100 | ;; memeq is like memcmp except it returns 0 (ne) or 1 (eq) 101 | (func $memeq (param $ptr1 i32) (param $ptr2 i32) (param $len i32) (result i32) 102 | (local $i1 i32) 103 | (local $i2 i32) 104 | (local.set $i1 (local.get $ptr1)) ;; i1 := ptr1 105 | (local.set $i2 (local.get $ptr2)) ;; i2 := ptr1 106 | 107 | (loop $len_gt_zero 108 | ;; if mem[i1] != mem[i2] 109 | (if (i32.ne (i32.load8_u (local.get $i1)) (i32.load8_u (local.get $i2))) 110 | (then (return (i32.const 0)))) ;; return 0 111 | 112 | (local.set $i1 (i32.add (local.get $i1) (i32.const 1))) ;; i1++ 113 | (local.set $i2 (i32.add (local.get $i2) (i32.const 1))) ;; i2++ 114 | (local.set $len (i32.sub (local.get $len) (i32.const 1))) ;; $len-- 115 | 116 | ;; if $len > 0 { continue } else { break } 117 | (br_if $len_gt_zero (i32.gt_u (local.get $len) (i32.const 0)))) 118 | 119 | (i32.const 1)) ;; return 1 120 | ) 121 | -------------------------------------------------------------------------------- /examples/wasi.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/examples/wasi.wasm -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/http-wasm/http-wasm-host-go 2 | 3 | go 1.21 4 | 5 | require github.com/tetratelabs/wazero v1.8.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g= 2 | github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= 3 | -------------------------------------------------------------------------------- /handler/cstring.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | 7 | wazeroapi "github.com/tetratelabs/wazero/api" 8 | 9 | "github.com/http-wasm/http-wasm-host-go/api/handler" 10 | ) 11 | 12 | func writeNULTerminated( 13 | ctx context.Context, 14 | mem wazeroapi.Memory, 15 | buf uint32, bufLimit handler.BufLimit, 16 | input []string, 17 | ) (countLen handler.CountLen) { 18 | count := uint32(len(input)) 19 | if count == 0 { 20 | return 21 | } 22 | 23 | byteCount := count // NUL terminator count 24 | for _, s := range input { 25 | byteCount += uint32(len(s)) 26 | } 27 | 28 | countLen = handler.CountLen(count)<<32 | handler.CountLen(byteCount) 29 | 30 | if byteCount > bufLimit { 31 | return // the guest can retry with a larger limit 32 | } 33 | 34 | // Write the NUL-terminated string to memory directly. 35 | s, ok := mem.Read(buf, byteCount) 36 | if !ok { 37 | panic("out of memory") // the guest passed a region outside memory. 38 | } 39 | 40 | b := bytes.NewBuffer(s) 41 | b.Reset() 42 | for _, h := range input { 43 | b.WriteString(h) 44 | b.WriteByte(0) 45 | } 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /handler/middleware_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/tetratelabs/wazero" 10 | "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 11 | 12 | "github.com/http-wasm/http-wasm-host-go/api/handler" 13 | "github.com/http-wasm/http-wasm-host-go/internal/test" 14 | ) 15 | 16 | var testCtx = context.Background() 17 | 18 | func TestNewMiddleware(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | guest []byte 22 | expectedError string 23 | }{ 24 | { 25 | name: "ok", 26 | guest: test.BinE2EProtocolVersion, 27 | }, 28 | { 29 | name: "panic on _start", 30 | guest: test.BinErrorPanicOnStart, 31 | expectedError: `wasm: error instantiating guest: module[1] function[_start] failed: wasm error: unreachable 32 | wasm stack trace: 33 | panic_on_start.main()`, 34 | }, 35 | } 36 | 37 | for _, tt := range tests { 38 | tc := tt 39 | t.Run(tc.name, func(t *testing.T) { 40 | t.Parallel() 41 | 42 | mw, err := NewMiddleware(testCtx, tc.guest, handler.UnimplementedHost{}) 43 | requireEqualError(t, err, tc.expectedError) 44 | if mw != nil { 45 | mw.Close(testCtx) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | type UnimplementedHostWithBufferFeature struct { 52 | handler.UnimplementedHost 53 | } 54 | 55 | func (UnimplementedHostWithBufferFeature) EnableFeatures(context.Context, handler.Features) handler.Features { 56 | return handler.FeatureBufferRequest 57 | } 58 | 59 | func TestAlreadyExistingWasipModulue(t *testing.T) { 60 | mw, err := NewMiddleware(testCtx, test.BinExampleWASI, UnimplementedHostWithBufferFeature{}, Runtime(func(ctx context.Context) (wazero.Runtime, error) { 61 | r := wazero.NewRuntime(context.Background()) 62 | _, err := wasi_snapshot_preview1.Instantiate(ctx, r) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | return r, nil 67 | })) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | if mw != nil { 73 | mw.Close(testCtx) 74 | } 75 | } 76 | 77 | func TestMiddlewareHandleRequest_Error(t *testing.T) { 78 | tests := []struct { 79 | name string 80 | guest []byte 81 | expectedError string 82 | }{ 83 | { 84 | name: "panic", 85 | guest: test.BinErrorPanicOnHandleRequest, 86 | expectedError: `wasm error: unreachable 87 | wasm stack trace: 88 | panic_on_handle_request.handle_request() i64`, 89 | }, 90 | } 91 | 92 | for _, tt := range tests { 93 | tc := tt 94 | t.Run(tc.name, func(t *testing.T) { 95 | mw, err := NewMiddleware(testCtx, tc.guest, handler.UnimplementedHost{}) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | defer mw.Close(testCtx) 100 | 101 | _, _, err = mw.HandleRequest(testCtx) 102 | requireEqualError(t, err, tc.expectedError) 103 | }) 104 | } 105 | } 106 | 107 | func TestMiddlewareHandleResponse_Error(t *testing.T) { 108 | tests := []struct { 109 | name string 110 | guest []byte 111 | expectedError string 112 | }{ 113 | { 114 | name: "panic", 115 | guest: test.BinErrorPanicOnHandleResponse, 116 | expectedError: `wasm error: unreachable 117 | wasm stack trace: 118 | panic_on_handle_response.handle_response(i32,i32)`, 119 | }, 120 | { 121 | name: "set_header_value request", 122 | guest: test.BinErrorSetRequestHeaderAfterNext, 123 | expectedError: `can't set request header after next handler (recovered by wazero) 124 | wasm stack trace: 125 | http_handler.set_header_value(i32,i32,i32,i32,i32) 126 | set_request_header_after_next.handle_response(i32,i32)`, 127 | }, 128 | } 129 | 130 | for _, tt := range tests { 131 | tc := tt 132 | t.Run(tc.name, func(t *testing.T) { 133 | mw, err := NewMiddleware(testCtx, tc.guest, handler.UnimplementedHost{}) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | defer mw.Close(testCtx) 138 | 139 | // We don't expect an error on the request path 140 | ctx, ctxNext, err := mw.HandleRequest(testCtx) 141 | requireHandleRequest(t, mw, ctxNext, err, 0) 142 | 143 | // We do expect an error on the response path 144 | err = mw.HandleResponse(ctx, 0, nil) 145 | requireEqualError(t, err, tc.expectedError) 146 | }) 147 | } 148 | } 149 | 150 | func TestMiddlewareResponseUsesRequestModule(t *testing.T) { 151 | mw, err := NewMiddleware(testCtx, test.BinE2EHandleResponse, handler.UnimplementedHost{}) 152 | if err != nil { 153 | t.Fatal(err) 154 | } 155 | defer mw.Close(testCtx) 156 | 157 | // A new guest module has initial state, so its value should be 42 158 | r1Ctx, ctxNext, err := mw.HandleRequest(testCtx) 159 | requireHandleRequest(t, mw, ctxNext, err, 42) 160 | 161 | // The first guest shouldn't return to the pool until HandleResponse, so 162 | // the second simultaneous call will get a new guest. 163 | r2Ctx, ctxNext2, err := mw.HandleRequest(testCtx) 164 | requireHandleRequest(t, mw, ctxNext2, err, 42) 165 | 166 | // Return the first request to the pool 167 | if err = mw.HandleResponse(r1Ctx, uint32(ctxNext>>32), nil); err != nil { 168 | t.Fatal(err) 169 | } 170 | requireGlobals(t, mw, 43) 171 | 172 | // The next request should re-use the returned module 173 | r3Ctx, ctxNext3, err := mw.HandleRequest(testCtx) 174 | requireHandleRequest(t, mw, ctxNext3, err, 43) 175 | if err = mw.HandleResponse(r3Ctx, uint32(ctxNext3>>32), nil); err != nil { 176 | t.Fatal(err) 177 | } 178 | requireGlobals(t, mw, 44) 179 | 180 | // Return the second request to the pool 181 | if err = mw.HandleResponse(r2Ctx, uint32(ctxNext2>>32), nil); err != nil { 182 | t.Fatal(err) 183 | } 184 | requireGlobals(t, mw, 44, 43) 185 | } 186 | 187 | func requireGlobals(t *testing.T, mw Middleware, wantGlobals ...uint64) { 188 | t.Helper() 189 | if want, have := wantGlobals, getGlobalVals(mw); !reflect.DeepEqual(want, have) { 190 | t.Errorf("unexpected globals, want: %v, have: %v", want, have) 191 | } 192 | } 193 | 194 | func getGlobalVals(mw Middleware) []uint64 { 195 | pool := mw.(*middleware).pool 196 | var guests []*guest 197 | var globals []uint64 198 | 199 | // Take all guests out of the pool 200 | for { 201 | if g, ok := pool.Get().(*guest); ok { 202 | guests = append(guests, g) 203 | continue 204 | } 205 | break 206 | } 207 | 208 | for _, g := range guests { 209 | v := g.guest.ExportedGlobal("reqCtx").Get() 210 | globals = append(globals, v) 211 | pool.Put(g) 212 | } 213 | 214 | return globals 215 | } 216 | 217 | func requireHandleRequest(t *testing.T, mw Middleware, ctxNext handler.CtxNext, err error, expectedCtx handler.CtxNext) { 218 | t.Helper() 219 | if err != nil { 220 | t.Fatal(err) 221 | } 222 | if want, have := expectedCtx, ctxNext>>32; want != have { 223 | t.Errorf("unexpected ctx, want: %d, have: %d", want, have) 224 | } 225 | if mw.(*middleware).pool.Get() != nil { 226 | t.Error("expected handler to not return guest to the pool") 227 | } 228 | } 229 | 230 | func requireEqualError(t *testing.T, err error, expectedError string) { 231 | if err != nil { 232 | if want, have := expectedError, err.Error(); want != have { 233 | t.Fatalf("unexpected error: want %v, have %v", want, have) 234 | } 235 | } else if want := expectedError; want != "" { 236 | t.Fatalf("expected error %v", want) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /handler/nethttp/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package wasm_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | _ "embed" 7 | "net/http" 8 | "strings" 9 | "testing" 10 | 11 | nethttp "github.com/http-wasm/http-wasm-host-go/handler/nethttp" 12 | "github.com/http-wasm/http-wasm-host-go/internal/test" 13 | ) 14 | 15 | var ( 16 | smallBody []byte 17 | largeSize int 18 | largeBody []byte 19 | ) 20 | 21 | func init() { 22 | noopHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 23 | smallBody = []byte("hello world") 24 | largeSize = 4096 // 2x the read buffer size 25 | largeBody = make([]byte, largeSize) 26 | for i := 0; i < largeSize/2; i++ { 27 | largeBody[i] = 'a' 28 | } 29 | for i := largeSize / 2; i < largeSize; i++ { 30 | largeBody[i] = 'b' 31 | } 32 | } 33 | 34 | func get(url string) (req *http.Request) { 35 | req, _ = http.NewRequest(http.MethodGet, url+"/v1.0/hi", nil) 36 | return 37 | } 38 | 39 | func getWithLargeHeader(url string) (req *http.Request) { 40 | req, _ = http.NewRequest(http.MethodGet, url+"/v1.0/hi", nil) 41 | req.Header.Add("data", string(largeBody)) 42 | return 43 | } 44 | 45 | func getWithQuery(url string) (req *http.Request) { 46 | req, _ = http.NewRequest(http.MethodGet, url+"/v1.0/hi?name=panda", nil) 47 | return 48 | } 49 | 50 | func getWithoutHeaders(url string) (req *http.Request) { 51 | req, _ = http.NewRequest(http.MethodGet, url+"/v1.0/hi", nil) 52 | req.Header = http.Header{} 53 | return 54 | } 55 | 56 | func post(url string) (req *http.Request) { 57 | body := bytes.NewReader(smallBody) 58 | req, _ = http.NewRequest(http.MethodPost, url, body) 59 | return 60 | } 61 | 62 | func postLarge(url string) (req *http.Request) { 63 | body := bytes.NewReader(largeBody) 64 | req, _ = http.NewRequest(http.MethodPost, url, body) 65 | return 66 | } 67 | 68 | func requestExampleWASI(url string) (req *http.Request) { 69 | body := strings.NewReader(`{"hello": "panda"}`) 70 | req, _ = http.NewRequest(http.MethodPost, url+"/v1.0/hi?name=panda", body) 71 | req.Header.Set("Content-Type", "application/json") 72 | req.Host = "localhost" 73 | return 74 | } 75 | 76 | var handlerExampleWASI = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 77 | w.Header().Set("Content-Type", "application/json") 78 | w.Header().Add("Set-Cookie", "a=b") // rewrite of multiple headers 79 | w.Header().Add("Set-Cookie", "c=d") 80 | 81 | // Use chunked encoding so we can set a test trailer 82 | w.Header().Set("Transfer-Encoding", "chunked") 83 | w.Header().Set("Trailer", "grpc-status") 84 | w.Header().Set(http.TrailerPrefix+"grpc-status", "1") 85 | w.Write([]byte(`{"hello": "world"}`)) // nolint 86 | }) 87 | 88 | var benches = map[string]struct { 89 | bin []byte 90 | next http.Handler 91 | request func(url string) *http.Request 92 | }{ 93 | "example wasi": { 94 | bin: test.BinExampleWASI, 95 | next: handlerExampleWASI, 96 | request: requestExampleWASI, 97 | }, 98 | "example router guest response": { 99 | bin: test.BinExampleRouter, 100 | request: func(url string) (req *http.Request) { 101 | req, _ = http.NewRequest(http.MethodGet, url, nil) 102 | return 103 | }, 104 | }, 105 | "example router host response": { 106 | bin: test.BinExampleRouter, 107 | request: func(url string) (req *http.Request) { 108 | req, _ = http.NewRequest(http.MethodGet, url+"/host", nil) 109 | return 110 | }, 111 | }, 112 | "log": { 113 | bin: test.BinBenchLog, 114 | request: get, 115 | }, 116 | "get_uri": { 117 | bin: test.BinBenchGetURI, 118 | request: get, 119 | }, 120 | "set_uri": { 121 | bin: test.BinBenchSetURI, 122 | request: get, 123 | }, 124 | "get_header_names none": { 125 | bin: test.BinBenchGetHeaderNames, 126 | request: getWithoutHeaders, 127 | }, 128 | "get_header_names": { 129 | bin: test.BinBenchGetHeaderNames, 130 | request: get, 131 | }, 132 | "get_header_names large": { 133 | bin: test.BinBenchGetHeaderNames, 134 | request: getWithLargeHeader, 135 | }, 136 | "get_header_values exists": { 137 | bin: test.BinBenchGetHeaderValues, 138 | request: get, 139 | }, 140 | "get_header_values not exists": { 141 | bin: test.BinBenchGetHeaderValues, 142 | request: getWithoutHeaders, 143 | }, 144 | "set_header_value": { 145 | bin: test.BinBenchSetHeaderValue, 146 | request: get, 147 | }, 148 | "add_header_value": { 149 | bin: test.BinBenchAddHeaderValue, 150 | request: get, 151 | }, 152 | "remove_header": { 153 | bin: test.BinBenchRemoveHeader, 154 | request: get, 155 | }, 156 | "read_body": { 157 | bin: test.BinBenchReadBody, 158 | request: post, 159 | }, 160 | "read_body_stream": { 161 | bin: test.BinBenchReadBodyStream, 162 | request: post, 163 | }, 164 | "read_body_stream large": { 165 | bin: test.BinBenchReadBodyStream, 166 | request: postLarge, 167 | }, 168 | "set_status_code": { 169 | bin: test.BinBenchSetStatusCode, 170 | request: get, 171 | }, 172 | "write_body": { 173 | bin: test.BinBenchWriteBody, 174 | request: get, 175 | }, 176 | } 177 | 178 | func Benchmark(b *testing.B) { 179 | for n, s := range benches { 180 | s := s 181 | b.Run(n, func(b *testing.B) { 182 | benchmark(b, n, s.bin, s.next, s.request) 183 | }) 184 | } 185 | } 186 | 187 | func benchmark(b *testing.B, name string, bin []byte, handler http.Handler, newRequest func(string) *http.Request) { 188 | ctx := context.Background() 189 | 190 | mw, err := nethttp.NewMiddleware(ctx, bin) 191 | if err != nil { 192 | b.Fatal(err) 193 | } 194 | defer mw.Close(ctx) 195 | 196 | if handler == nil { 197 | handler = noopHandler 198 | } 199 | h := mw.NewHandler(ctx, handler) 200 | 201 | b.Run(name, func(b *testing.B) { 202 | // We don't report allocations because memory allocations for TinyGo are 203 | // in wasm which isn't visible to the Go benchmark. 204 | for i := 0; i < b.N; i++ { 205 | h.ServeHTTP(fakeResponseWriter{}, newRequest("http://localhost")) 206 | } 207 | }) 208 | } 209 | 210 | var _ http.ResponseWriter = fakeResponseWriter{} 211 | 212 | type fakeResponseWriter struct{} 213 | 214 | func (rw fakeResponseWriter) Header() http.Header { 215 | return http.Header{} 216 | } 217 | 218 | func (rw fakeResponseWriter) Write(b []byte) (int, error) { 219 | return len(b), nil 220 | } 221 | 222 | func (rw fakeResponseWriter) WriteHeader(statusCode int) { 223 | // None of our benchmark tests should send failure status. If there's a 224 | // failure, it is likely there's a problem in the test data. 225 | if statusCode == 500 { 226 | panic(statusCode) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /handler/nethttp/buffer.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | type bufferingRequestBody struct { 10 | delegate io.ReadCloser 11 | buffer bytes.Buffer 12 | } 13 | 14 | // Read buffers anything read from the delegate. 15 | func (b *bufferingRequestBody) Read(p []byte) (n int, err error) { 16 | n, err = b.delegate.Read(p) 17 | if err != nil && n > 0 { 18 | b.buffer.Write(p[0:n]) 19 | } 20 | return 21 | } 22 | 23 | // Close dispatches to the delegate. 24 | func (b *bufferingRequestBody) Close() (err error) { 25 | if b.delegate != nil { 26 | err = b.delegate.Close() 27 | } 28 | return 29 | } 30 | 31 | type bufferingResponseWriter struct { 32 | delegate http.ResponseWriter 33 | statusCode uint32 34 | body []byte 35 | } 36 | 37 | // Header dispatches to the delegate. 38 | func (w *bufferingResponseWriter) Header() http.Header { 39 | return w.delegate.Header() 40 | } 41 | 42 | // Write buffers the response body. 43 | func (w *bufferingResponseWriter) Write(bytes []byte) (int, error) { 44 | w.body = append(w.body, bytes...) 45 | return len(bytes), nil 46 | } 47 | 48 | // WriteHeader buffers the status code. 49 | func (w *bufferingResponseWriter) WriteHeader(statusCode int) { 50 | w.statusCode = uint32(statusCode) 51 | } 52 | 53 | // release sends any response data collected. 54 | func (w *bufferingResponseWriter) release() { 55 | // If we deferred the response, release it. 56 | if statusCode := w.statusCode; statusCode != 0 { 57 | w.delegate.WriteHeader(int(statusCode)) 58 | } 59 | if body := w.body; len(body) != 0 { 60 | w.delegate.Write(body) // nolint 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /handler/nethttp/buffer_test.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | ) 7 | 8 | // compile-time check to ensure bufferingRequestBody implements io.ReadCloser. 9 | var _ io.ReadCloser = &bufferingRequestBody{} 10 | 11 | // compile-time check to ensure bufferingResponseWriter implements 12 | // http.ResponseWriter. 13 | var _ http.ResponseWriter = &bufferingResponseWriter{} 14 | -------------------------------------------------------------------------------- /handler/nethttp/example_test.go: -------------------------------------------------------------------------------- 1 | package wasm_test 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "net/http/httptest" 11 | "os" 12 | "strings" 13 | 14 | "github.com/tetratelabs/wazero" 15 | 16 | "github.com/http-wasm/http-wasm-host-go/api" 17 | "github.com/http-wasm/http-wasm-host-go/handler" 18 | wasm "github.com/http-wasm/http-wasm-host-go/handler/nethttp" 19 | "github.com/http-wasm/http-wasm-host-go/internal/test" 20 | ) 21 | 22 | var ( 23 | requestBody = "{\"hello\": \"panda\"}" 24 | responseBody = "{\"hello\": \"world\"}" 25 | 26 | serveJson = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | r.Header.Set("Content-Type", "application/json") 28 | w.Write([]byte(responseBody)) // nolint 29 | }) 30 | 31 | servePath = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 | r.Header.Set("Content-Type", "text/plain") 33 | w.Write([]byte(r.URL.Path)) // nolint 34 | }) 35 | ) 36 | 37 | func Example_auth() { 38 | ctx := context.Background() 39 | 40 | // Configure and compile the WebAssembly guest binary. In this case, it is 41 | // an auth interceptor. 42 | mw, err := wasm.NewMiddleware(ctx, test.BinExampleAuth) 43 | if err != nil { 44 | log.Panicln(err) 45 | } 46 | defer mw.Close(ctx) 47 | 48 | // Create the real request handler. 49 | next := serveJson 50 | 51 | // Wrap this with an interceptor implemented in WebAssembly. 52 | wrapped := mw.NewHandler(ctx, next) 53 | 54 | // Start the server with the wrapped handler. 55 | ts := httptest.NewServer(wrapped) 56 | defer ts.Close() 57 | 58 | // Invoke some requests, only one of which should pass 59 | headers := []http.Header{ 60 | {"NotAuthorization": {"1"}}, 61 | {"Authorization": {""}}, 62 | {"Authorization": {"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}}, 63 | {"Authorization": {"0"}}, 64 | } 65 | 66 | for _, header := range headers { 67 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 68 | if err != nil { 69 | log.Panicln(err) 70 | } 71 | req.Header = header 72 | 73 | resp, err := ts.Client().Do(req) 74 | if err != nil { 75 | log.Panicln(err) 76 | } 77 | resp.Body.Close() 78 | 79 | switch resp.StatusCode { 80 | case http.StatusOK: 81 | fmt.Println("OK") 82 | case http.StatusUnauthorized: 83 | fmt.Println("Unauthorized") 84 | default: 85 | log.Panicln("unexpected status code", resp.StatusCode) 86 | } 87 | if auth, ok := resp.Header["Www-Authenticate"]; ok { 88 | fmt.Println("Www-Authenticate:", auth[0]) 89 | } 90 | } 91 | 92 | // Output: 93 | // Unauthorized 94 | // Www-Authenticate: Basic realm="test" 95 | // Unauthorized 96 | // OK 97 | // Unauthorized 98 | } 99 | 100 | func Example_wasi() { 101 | ctx := context.Background() 102 | moduleConfig := wazero.NewModuleConfig().WithStdout(os.Stdout) 103 | 104 | // Configure and compile the WebAssembly guest binary. In this case, it 105 | // prints the request and response to the STDOUT via WASI. 106 | mw, err := wasm.NewMiddleware(ctx, test.BinExampleWASI, 107 | handler.ModuleConfig(moduleConfig)) 108 | if err != nil { 109 | log.Panicln(err) 110 | } 111 | defer mw.Close(ctx) 112 | 113 | // Create the real request handler. 114 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 115 | w.Header().Set("Content-Type", "application/json") 116 | w.Header().Add("Set-Cookie", "a=b") // example of multiple headers 117 | w.Header().Add("Set-Cookie", "c=d") 118 | w.Header().Set("Date", "Tue, 15 Nov 1994 08:12:31 GMT") 119 | 120 | // Use chunked encoding so we can set a test trailer 121 | w.Header().Set("Transfer-Encoding", "chunked") 122 | w.Header().Set("Trailer", "grpc-status") 123 | w.Header().Set(http.TrailerPrefix+"grpc-status", "1") 124 | w.Write([]byte(`{"hello": "world"}`)) // nolint 125 | }) 126 | 127 | // Wrap this with an interceptor implemented in WebAssembly. 128 | wrapped := mw.NewHandler(ctx, next) 129 | 130 | // Start the server with the wrapped handler. 131 | ts := httptest.NewServer(wrapped) 132 | defer ts.Close() 133 | 134 | // Make a client request which should print to the console 135 | req, err := http.NewRequest("POST", ts.URL, strings.NewReader(requestBody)) 136 | if err != nil { 137 | log.Panicln(err) 138 | } 139 | req.Header.Set("Content-Type", "application/json") 140 | req.Host = "localhost" 141 | resp, err := ts.Client().Do(req) 142 | if err != nil { 143 | log.Panicln(err) 144 | } 145 | defer resp.Body.Close() 146 | 147 | // Output: 148 | // POST / HTTP/1.1 149 | // accept-encoding: gzip 150 | // content-length: 18 151 | // content-type: application/json 152 | // host: localhost 153 | // user-agent: Go-http-client/1.1 154 | // 155 | // {"hello": "panda"} 156 | // 157 | // HTTP/1.1 200 158 | // content-type: application/json 159 | // date: Tue, 15 Nov 1994 08:12:31 GMT 160 | // set-cookie: a=b 161 | // set-cookie: c=d 162 | // trailer: grpc-status 163 | // transfer-encoding: chunked 164 | // 165 | // {"hello": "world"} 166 | // grpc-status: 1 167 | } 168 | 169 | func Example_log() { 170 | ctx := context.Background() 171 | 172 | // Configure and compile the WebAssembly guest binary. In this case, it is 173 | // a logging interceptor. 174 | mw, err := wasm.NewMiddleware(ctx, test.BinExampleLog, handler.Logger(api.ConsoleLogger{})) 175 | if err != nil { 176 | log.Panicln(err) 177 | } 178 | defer mw.Close(ctx) 179 | 180 | // Create the real request handler. 181 | next := serveJson 182 | 183 | // Wrap this with an interceptor implemented in WebAssembly. 184 | wrapped := mw.NewHandler(ctx, next) 185 | 186 | // Start the server with the wrapped handler. 187 | ts := httptest.NewServer(wrapped) 188 | defer ts.Close() 189 | 190 | // Make a client request. 191 | resp, err := ts.Client().Get(ts.URL) 192 | if err != nil { 193 | log.Panicln(err) 194 | } 195 | defer resp.Body.Close() 196 | 197 | // Output: 198 | // hello world 199 | } 200 | 201 | func Example_router() { 202 | ctx := context.Background() 203 | 204 | // Configure and compile the WebAssembly guest binary. In this case, it is 205 | // an example request router. 206 | mw, err := wasm.NewMiddleware(ctx, test.BinExampleRouter) 207 | if err != nil { 208 | log.Panicln(err) 209 | } 210 | defer mw.Close(ctx) 211 | 212 | // Wrap the real handler with an interceptor implemented in WebAssembly. 213 | wrapped := mw.NewHandler(ctx, servePath) 214 | 215 | // Start the server with the wrapped handler. 216 | ts := httptest.NewServer(wrapped) 217 | defer ts.Close() 218 | 219 | // Invoke some requests, only one of which should pass 220 | paths := []string{ 221 | "", 222 | "nothosst", 223 | "host/a", 224 | } 225 | 226 | for _, p := range paths { 227 | url := fmt.Sprintf("%s/%s", ts.URL, p) 228 | resp, err := ts.Client().Get(url) 229 | if err != nil { 230 | log.Panicln(err) 231 | } 232 | defer resp.Body.Close() 233 | content, _ := io.ReadAll(resp.Body) 234 | fmt.Println(string(content)) 235 | } 236 | 237 | // Output: 238 | // hello world 239 | // hello world 240 | // /a 241 | } 242 | 243 | func Example_redact() { 244 | ctx := context.Background() 245 | 246 | // Configure and compile the WebAssembly guest binary. In this case, it is 247 | // an example response redact. 248 | secret := "open sesame" 249 | mw, err := wasm.NewMiddleware(ctx, test.BinExampleRedact, 250 | handler.GuestConfig([]byte(secret))) 251 | if err != nil { 252 | log.Panicln(err) 253 | } 254 | defer mw.Close(ctx) 255 | 256 | var body string 257 | serveBody := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 258 | content, _ := io.ReadAll(r.Body) 259 | fmt.Println(string(content)) 260 | r.Header.Set("Content-Type", "text/plain") 261 | w.Write([]byte(body)) // nolint 262 | }) 263 | 264 | // Wrap the real handler with an interceptor implemented in WebAssembly. 265 | wrapped := mw.NewHandler(ctx, serveBody) 266 | 267 | // Start the server with the wrapped handler. 268 | ts := httptest.NewServer(wrapped) 269 | defer ts.Close() 270 | 271 | bodies := []string{ 272 | secret, 273 | "hello world", 274 | fmt.Sprintf("hello %s world", secret), 275 | } 276 | 277 | for _, b := range bodies { 278 | body = b 279 | 280 | resp, err := ts.Client().Post(ts.URL, "text/plain", strings.NewReader(body)) 281 | if err != nil { 282 | log.Panicln(err) 283 | } 284 | defer resp.Body.Close() 285 | content, _ := io.ReadAll(resp.Body) 286 | fmt.Println(string(content)) 287 | } 288 | 289 | // Output: 290 | // ########### 291 | // ########### 292 | // hello world 293 | // hello world 294 | // hello ########### world 295 | // hello ########### world 296 | } 297 | -------------------------------------------------------------------------------- /handler/nethttp/host.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "net/http" 8 | "net/textproto" 9 | "net/url" 10 | "sort" 11 | "strings" 12 | 13 | "github.com/http-wasm/http-wasm-host-go/api/handler" 14 | ) 15 | 16 | type host struct{} 17 | 18 | var _ handler.Host = host{} 19 | 20 | // EnableFeatures implements the same method as documented on handler.Host. 21 | func (host) EnableFeatures(ctx context.Context, features handler.Features) handler.Features { 22 | if s, ok := ctx.Value(requestStateKey{}).(*requestState); ok { 23 | s.enableFeatures(features) 24 | } 25 | // Otherwise, this was called during init, but there's nothing to do 26 | // because net/http supports all features. 27 | return features 28 | } 29 | 30 | // GetMethod implements the same method as documented on handler.Host. 31 | func (host) GetMethod(ctx context.Context) string { 32 | r := requestStateFromContext(ctx).r 33 | return r.Method 34 | } 35 | 36 | // SetMethod implements the same method as documented on handler.Host. 37 | func (host) SetMethod(ctx context.Context, method string) { 38 | r := requestStateFromContext(ctx).r 39 | r.Method = method 40 | } 41 | 42 | // GetURI implements the same method as documented on handler.Host. 43 | func (host) GetURI(ctx context.Context) string { 44 | r := requestStateFromContext(ctx).r 45 | u := r.URL 46 | result := u.EscapedPath() 47 | if result == "" { 48 | result = "/" 49 | } 50 | if u.ForceQuery || u.RawQuery != "" { 51 | result += "?" + u.RawQuery 52 | } 53 | return result 54 | } 55 | 56 | // SetURI implements the same method as documented on handler.Host. 57 | func (host) SetURI(ctx context.Context, uri string) { 58 | r := requestStateFromContext(ctx).r 59 | if uri == "" { // url.ParseRequestURI fails on empty 60 | r.RequestURI = "/" 61 | r.URL.RawPath = "/" 62 | r.URL.Path = "/" 63 | r.URL.ForceQuery = false 64 | r.URL.RawQuery = "" 65 | return 66 | } 67 | u, err := url.ParseRequestURI(uri) 68 | if err != nil { 69 | panic(err) 70 | } 71 | r.RequestURI = uri 72 | r.URL.RawPath = u.RawPath 73 | r.URL.Path = u.Path 74 | r.URL.ForceQuery = u.ForceQuery 75 | r.URL.RawQuery = u.RawQuery 76 | } 77 | 78 | // GetProtocolVersion implements the same method as documented on handler.Host. 79 | func (host) GetProtocolVersion(ctx context.Context) string { 80 | r := requestStateFromContext(ctx).r 81 | return r.Proto 82 | } 83 | 84 | // GetRequestHeaderNames implements the same method as documented on handler.Host. 85 | func (host) GetRequestHeaderNames(ctx context.Context) (names []string) { 86 | r := requestStateFromContext(ctx).r 87 | 88 | count := len(r.Header) 89 | i := 0 90 | if r.Host != "" { // special-case the host header. 91 | count++ 92 | names = make([]string, count) 93 | names[i] = "Host" 94 | i++ 95 | } else if count == 0 { 96 | return nil 97 | } else { 98 | names = make([]string, count) 99 | } 100 | 101 | for n := range r.Header { 102 | if strings.HasPrefix(n, http.TrailerPrefix) { 103 | continue 104 | } 105 | names[i] = n 106 | i++ 107 | } 108 | 109 | if len(names) == 0 { // E.g. only trailers 110 | return nil 111 | } 112 | 113 | // Keys in a Go map don't have consistent ordering. 114 | sort.Strings(names) 115 | return 116 | } 117 | 118 | // GetRequestHeaderValues implements the same method as documented on handler.Host. 119 | func (host) GetRequestHeaderValues(ctx context.Context, name string) []string { 120 | r := requestStateFromContext(ctx).r 121 | if textproto.CanonicalMIMEHeaderKey(name) == "Host" { // special-case the host header. 122 | return []string{r.Host} 123 | } 124 | return r.Header.Values(name) 125 | } 126 | 127 | // SetRequestHeaderValue implements the same method as documented on handler.Host. 128 | func (host) SetRequestHeaderValue(ctx context.Context, name, value string) { 129 | s := requestStateFromContext(ctx) 130 | s.r.Header.Set(name, value) 131 | } 132 | 133 | // AddRequestHeaderValue implements the same method as documented on handler.Host. 134 | func (host) AddRequestHeaderValue(ctx context.Context, name, value string) { 135 | s := requestStateFromContext(ctx) 136 | s.r.Header.Add(name, value) 137 | } 138 | 139 | // RemoveRequestHeader implements the same method as documented on handler.Host. 140 | func (host) RemoveRequestHeader(ctx context.Context, name string) { 141 | s := requestStateFromContext(ctx) 142 | s.r.Header.Del(name) 143 | } 144 | 145 | // RequestBodyReader implements the same method as documented on handler.Host. 146 | func (host) RequestBodyReader(ctx context.Context) io.ReadCloser { 147 | s := requestStateFromContext(ctx) 148 | return s.r.Body 149 | } 150 | 151 | // RequestBodyWriter implements the same method as documented on handler.Host. 152 | func (host) RequestBodyWriter(ctx context.Context) io.Writer { 153 | s := requestStateFromContext(ctx) 154 | var b bytes.Buffer // reset 155 | s.r.Body = io.NopCloser(&b) 156 | return &b 157 | } 158 | 159 | // GetRequestTrailerNames implements the same method as documented on 160 | // handler.Host. 161 | func (host) GetRequestTrailerNames(ctx context.Context) (names []string) { 162 | header := requestStateFromContext(ctx).w.Header() 163 | return trailerNames(header) 164 | } 165 | 166 | // GetRequestTrailerValues implements the same method as documented on 167 | // handler.Host. 168 | func (host) GetRequestTrailerValues(ctx context.Context, name string) []string { 169 | header := requestStateFromContext(ctx).w.Header() 170 | return getTrailers(header, name) 171 | } 172 | 173 | // SetRequestTrailerValue implements the same method as documented on 174 | // handler.Host. 175 | func (host) SetRequestTrailerValue(ctx context.Context, name, value string) { 176 | header := requestStateFromContext(ctx).w.Header() 177 | setTrailer(header, name, value) 178 | } 179 | 180 | // AddRequestTrailerValue implements the same method as documented on 181 | // handler.Host. 182 | func (host) AddRequestTrailerValue(ctx context.Context, name, value string) { 183 | header := requestStateFromContext(ctx).w.Header() 184 | addTrailer(header, name, value) 185 | } 186 | 187 | // RemoveRequestTrailer implements the same method as documented on handler.Host. 188 | func (host) RemoveRequestTrailer(ctx context.Context, name string) { 189 | header := requestStateFromContext(ctx).w.Header() 190 | removeTrailer(header, name) 191 | } 192 | 193 | // GetStatusCode implements the same method as documented on handler.Host. 194 | func (host) GetStatusCode(ctx context.Context) uint32 { 195 | s := requestStateFromContext(ctx) 196 | if statusCode := s.w.(*bufferingResponseWriter).statusCode; statusCode == 0 { 197 | return 200 // default 198 | } else { 199 | return statusCode 200 | } 201 | } 202 | 203 | // SetStatusCode implements the same method as documented on handler.Host. 204 | func (host) SetStatusCode(ctx context.Context, statusCode uint32) { 205 | s := requestStateFromContext(ctx) 206 | if w, ok := s.w.(*bufferingResponseWriter); ok { 207 | w.statusCode = statusCode 208 | } else { 209 | s.w.WriteHeader(int(statusCode)) 210 | } 211 | } 212 | 213 | // GetResponseHeaderNames implements the same method as documented on 214 | // handler.Host. 215 | func (host) GetResponseHeaderNames(ctx context.Context) (names []string) { 216 | w := requestStateFromContext(ctx).w 217 | 218 | // allocate capacity == count though it might be smaller due to trailers. 219 | count := len(w.Header()) 220 | if count == 0 { 221 | return nil 222 | } 223 | 224 | names = make([]string, 0, count) 225 | 226 | for n := range w.Header() { 227 | if strings.HasPrefix(n, http.TrailerPrefix) { 228 | continue 229 | } 230 | names = append(names, n) 231 | } 232 | 233 | if len(names) == 0 { // E.g. only trailers 234 | return nil 235 | } 236 | // Keys in a Go map don't have consistent ordering. 237 | sort.Strings(names) 238 | return 239 | } 240 | 241 | // GetResponseHeaderValues implements the same method as documented on 242 | // handler.Host. 243 | func (host) GetResponseHeaderValues(ctx context.Context, name string) []string { 244 | w := requestStateFromContext(ctx).w 245 | return w.Header().Values(name) 246 | } 247 | 248 | // SetResponseHeaderValue implements the same method as documented on 249 | // handler.Host. 250 | func (host) SetResponseHeaderValue(ctx context.Context, name, value string) { 251 | s := requestStateFromContext(ctx) 252 | s.w.Header().Set(name, value) 253 | } 254 | 255 | // AddResponseHeaderValue implements the same method as documented on 256 | // handler.Host. 257 | func (host) AddResponseHeaderValue(ctx context.Context, name, value string) { 258 | s := requestStateFromContext(ctx) 259 | s.w.Header().Add(name, value) 260 | } 261 | 262 | // RemoveResponseHeader implements the same method as documented on 263 | // handler.Host. 264 | func (host) RemoveResponseHeader(ctx context.Context, name string) { 265 | s := requestStateFromContext(ctx) 266 | s.w.Header().Del(name) 267 | } 268 | 269 | // ResponseBodyReader implements the same method as documented on handler.Host. 270 | func (host) ResponseBodyReader(ctx context.Context) io.ReadCloser { 271 | s := requestStateFromContext(ctx) 272 | body := s.w.(*bufferingResponseWriter).body 273 | return io.NopCloser(bytes.NewReader(body)) 274 | } 275 | 276 | // ResponseBodyWriter implements the same method as documented on handler.Host. 277 | func (host) ResponseBodyWriter(ctx context.Context) io.Writer { 278 | s := requestStateFromContext(ctx) 279 | if w, ok := s.w.(*bufferingResponseWriter); ok { 280 | w.body = nil // reset 281 | return w 282 | } else { 283 | return s.w 284 | } 285 | } 286 | 287 | // GetResponseTrailerNames implements the same method as documented on 288 | // handler.Host. 289 | func (host) GetResponseTrailerNames(ctx context.Context) (names []string) { 290 | header := requestStateFromContext(ctx).w.Header() 291 | return trailerNames(header) 292 | } 293 | 294 | // GetResponseTrailerValues implements the same method as documented on 295 | // handler.Host. 296 | func (host) GetResponseTrailerValues(ctx context.Context, name string) []string { 297 | header := requestStateFromContext(ctx).w.Header() 298 | return getTrailers(header, name) 299 | } 300 | 301 | // SetResponseTrailerValue implements the same method as documented on 302 | // handler.Host. 303 | func (host) SetResponseTrailerValue(ctx context.Context, name, value string) { 304 | header := requestStateFromContext(ctx).w.Header() 305 | setTrailer(header, name, value) 306 | } 307 | 308 | // AddResponseTrailerValue implements the same method as documented on 309 | // handler.Host. 310 | func (host) AddResponseTrailerValue(ctx context.Context, name, value string) { 311 | header := requestStateFromContext(ctx).w.Header() 312 | addTrailer(header, name, value) 313 | } 314 | 315 | // RemoveResponseTrailer implements the same method as documented on handler.Host. 316 | func (host) RemoveResponseTrailer(ctx context.Context, name string) { 317 | header := requestStateFromContext(ctx).w.Header() 318 | removeTrailer(header, name) 319 | } 320 | 321 | func trailerNames(header http.Header) (names []string) { 322 | // We don't pre-allocate as there may be no trailers. 323 | for n := range header { 324 | if strings.HasPrefix(n, http.TrailerPrefix) { 325 | n = n[len(http.TrailerPrefix):] 326 | names = append(names, n) 327 | } 328 | } 329 | // Keys in a Go map don't have consistent ordering. 330 | sort.Strings(names) 331 | return 332 | } 333 | 334 | func getTrailers(header http.Header, name string) []string { 335 | return header.Values(http.TrailerPrefix + name) 336 | } 337 | 338 | func setTrailer(header http.Header, name string, value string) { 339 | header.Set(http.TrailerPrefix+name, value) 340 | } 341 | 342 | func addTrailer(header http.Header, name string, value string) { 343 | header.Set(http.TrailerPrefix+name, value) 344 | } 345 | 346 | func removeTrailer(header http.Header, name string) { 347 | header.Del(http.TrailerPrefix + name) 348 | } 349 | 350 | // GetSourceAddr implements the same method as documented on handler.Host. 351 | func (host) GetSourceAddr(ctx context.Context) string { 352 | r := requestStateFromContext(ctx).r 353 | return r.RemoteAddr 354 | } 355 | -------------------------------------------------------------------------------- /handler/nethttp/host_test.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/http-wasm/http-wasm-host-go/api/handler" 11 | "github.com/http-wasm/http-wasm-host-go/testing/handlertest" 12 | ) 13 | 14 | var testCtx = context.Background() 15 | 16 | func Test_host(t *testing.T) { 17 | newCtx := func(features handler.Features) (context.Context, handler.Features) { 18 | // The below configuration supports all features. 19 | r, _ := http.NewRequest("GET", "", bytes.NewReader(nil)) 20 | r.RemoteAddr = "1.2.3.4:12345" 21 | w := &bufferingResponseWriter{delegate: &httptest.ResponseRecorder{HeaderMap: map[string][]string{}}} 22 | return context.WithValue(testCtx, requestStateKey{}, &requestState{r: r, w: w}), features 23 | } 24 | 25 | if err := handlertest.HostTest(t, host{}, newCtx); err != nil { 26 | t.Fatal(err) 27 | } 28 | } 29 | 30 | // Test_host_GetProtocolVersion ensures HTTP/2.0 is readable 31 | func Test_host_GetProtocolVersion(t *testing.T) { 32 | tests := []string{"HTTP/1.1", "HTTP/2.0"} 33 | 34 | h := host{} 35 | for _, tt := range tests { 36 | tc := tt 37 | t.Run(tc, func(t *testing.T) { 38 | r := &http.Request{Proto: tc} 39 | ctx := context.WithValue(testCtx, requestStateKey{}, &requestState{r: r}) 40 | 41 | if want, have := tc, h.GetProtocolVersion(ctx); want != have { 42 | t.Errorf("unexpected protocolVersion, want: %v, have: %v", want, have) 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /handler/nethttp/middleware.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | handlerapi "github.com/http-wasm/http-wasm-host-go/api/handler" 10 | "github.com/http-wasm/http-wasm-host-go/handler" 11 | ) 12 | 13 | // compile-time checks to ensure interfaces are implemented. 14 | var _ http.Handler = (*guest)(nil) 15 | 16 | type Middleware handlerapi.Middleware[http.Handler] 17 | 18 | type middleware struct { 19 | m handler.Middleware 20 | } 21 | 22 | func NewMiddleware(ctx context.Context, guest []byte, options ...handler.Option) (Middleware, error) { 23 | m, err := handler.NewMiddleware(ctx, guest, host{}, options...) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return &middleware{m: m}, nil 29 | } 30 | 31 | // requestStateKey is a context.Context value associated with a requestState 32 | // pointer to the current request. 33 | type requestStateKey struct{} 34 | 35 | type requestState struct { 36 | w http.ResponseWriter 37 | r *http.Request 38 | next http.Handler 39 | features handlerapi.Features 40 | } 41 | 42 | func newRequestState(w http.ResponseWriter, r *http.Request, g *guest) *requestState { 43 | s := &requestState{w: w, r: r, next: g.next} 44 | s.enableFeatures(g.features) 45 | return s 46 | } 47 | 48 | func (s *requestState) enableFeatures(features handlerapi.Features) { 49 | s.features = s.features.WithEnabled(features) 50 | if features.IsEnabled(handlerapi.FeatureBufferRequest) { 51 | s.r.Body = &bufferingRequestBody{delegate: s.r.Body} 52 | } 53 | if s.features.IsEnabled(handlerapi.FeatureBufferResponse) { 54 | if _, ok := s.w.(*bufferingResponseWriter); !ok { // don't double-wrap 55 | s.w = &bufferingResponseWriter{delegate: s.w} 56 | } 57 | } 58 | } 59 | 60 | func (s *requestState) handleNext() (err error) { 61 | defer func() { 62 | if recovered := recover(); recovered != nil { 63 | if e, ok := recovered.(error); ok { 64 | err = e 65 | } else { 66 | err = fmt.Errorf("%v", recovered) 67 | } 68 | } 69 | }() 70 | 71 | // If we set the intercepted the request body for any reason, reset it 72 | // before calling downstream. 73 | if br, ok := s.r.Body.(*bufferingRequestBody); ok { 74 | if br.buffer.Len() == 0 { 75 | s.r.Body = br.delegate 76 | } else { 77 | br.Close() // nolint 78 | s.r.Body = io.NopCloser(&br.buffer) 79 | } 80 | } 81 | s.next.ServeHTTP(s.w, s.r) 82 | return 83 | } 84 | 85 | func requestStateFromContext(ctx context.Context) *requestState { 86 | return ctx.Value(requestStateKey{}).(*requestState) 87 | } 88 | 89 | // NewHandler implements the same method as documented on handler.Middleware. 90 | func (w *middleware) NewHandler(_ context.Context, next http.Handler) http.Handler { 91 | return &guest{ 92 | handleRequest: w.m.HandleRequest, 93 | handleResponse: w.m.HandleResponse, 94 | next: next, 95 | features: w.m.Features(), 96 | } 97 | } 98 | 99 | // Close implements the same method as documented on handler.Middleware. 100 | func (w *middleware) Close(ctx context.Context) error { 101 | return w.m.Close(ctx) 102 | } 103 | 104 | type guest struct { 105 | handleRequest func(ctx context.Context) (outCtx context.Context, ctxNext handlerapi.CtxNext, err error) 106 | handleResponse func(ctx context.Context, reqCtx uint32, err error) error 107 | next http.Handler 108 | features handlerapi.Features 109 | } 110 | 111 | // ServeHTTP implements http.Handler 112 | func (g *guest) ServeHTTP(w http.ResponseWriter, r *http.Request) { 113 | // The guest Wasm actually handles the request. As it may call host 114 | // functions, we add context parameters of the current request. 115 | s := newRequestState(w, r, g) 116 | ctx := context.WithValue(r.Context(), requestStateKey{}, s) 117 | outCtx, ctxNext, requestErr := g.handleRequest(ctx) 118 | if requestErr != nil { 119 | handleErr(w, requestErr) 120 | } 121 | 122 | // If buffering was enabled, ensure it flushes. 123 | if bw, ok := s.w.(*bufferingResponseWriter); ok { 124 | defer bw.release() 125 | } 126 | 127 | // Returning zero means the guest wants to break the handler chain, and 128 | // handle the response directly. 129 | if uint32(ctxNext) == 0 { 130 | return 131 | } 132 | 133 | // Otherwise, the host calls the next handler. 134 | err := s.handleNext() 135 | 136 | // Finally, call the guest with the response or error 137 | if err = g.handleResponse(outCtx, uint32(ctxNext>>32), err); err != nil { 138 | panic(err) 139 | } 140 | } 141 | 142 | func handleErr(w http.ResponseWriter, requestErr error) { 143 | // TODO: after testing, shouldn't send errors into the HTTP response. 144 | w.WriteHeader(500) 145 | w.Write([]byte(requestErr.Error())) // nolint 146 | } 147 | -------------------------------------------------------------------------------- /handler/nethttp/middleware_test.go: -------------------------------------------------------------------------------- 1 | package wasm_test 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | 12 | handlerapi "github.com/http-wasm/http-wasm-host-go/api/handler" 13 | "github.com/http-wasm/http-wasm-host-go/handler" 14 | wasm "github.com/http-wasm/http-wasm-host-go/handler/nethttp" 15 | "github.com/http-wasm/http-wasm-host-go/internal/test" 16 | ) 17 | 18 | var ( 19 | testCtx = context.Background() 20 | noopHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 21 | ) 22 | 23 | func TestConfig(t *testing.T) { 24 | tests := []handlerapi.Features{ 25 | 0, 26 | handlerapi.FeatureBufferRequest, 27 | handlerapi.FeatureBufferResponse, 28 | handlerapi.FeatureTrailers, 29 | handlerapi.FeatureBufferRequest | handlerapi.FeatureBufferResponse, 30 | handlerapi.FeatureBufferRequest | handlerapi.FeatureBufferResponse | handlerapi.FeatureTrailers, 31 | } 32 | 33 | for _, tt := range tests { 34 | tc := tt 35 | t.Run(tc.String(), func(t *testing.T) { 36 | guestConfig := make([]byte, 8) 37 | binary.LittleEndian.PutUint64(guestConfig, uint64(tc)) 38 | mw, err := wasm.NewMiddleware(testCtx, test.BinExampleConfig, handler.GuestConfig(guestConfig)) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | defer mw.Close(testCtx) 43 | 44 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 45 | // ensure the request body is readable 46 | body, err := io.ReadAll(r.Body) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | if want, have := requestBody, string(body); want != have { 51 | t.Fatalf("unexpected request body, want: %q, have: %q", want, have) 52 | } 53 | r.Header.Set("Content-Type", "application/json") 54 | w.Write([]byte(responseBody)) // nolint 55 | }) 56 | 57 | ts := httptest.NewServer(mw.NewHandler(testCtx, next)) 58 | defer ts.Close() 59 | 60 | resp, err := ts.Client().Post(ts.URL, "application/json", strings.NewReader(requestBody)) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | defer resp.Body.Close() 65 | 66 | // Ensure the response body was still readable! 67 | body, err := io.ReadAll(resp.Body) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | if want, have := responseBody, string(body); want != have { 72 | t.Fatalf("unexpected response body, want: %q, have: %q", want, have) 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func TestProtocolVersion(t *testing.T) { 79 | mw, err := wasm.NewMiddleware(testCtx, test.BinE2EProtocolVersion) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | defer mw.Close(testCtx) 84 | 85 | ts := httptest.NewServer(mw.NewHandler(testCtx, noopHandler)) 86 | defer ts.Close() 87 | 88 | resp, err := ts.Client().Get(ts.URL) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | defer resp.Body.Close() 93 | 94 | body, err := io.ReadAll(resp.Body) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | if want, have := "HTTP/1.1", string(body); want != have { 99 | t.Fatalf("unexpected protocol version, want: %q, have: %q", want, have) 100 | } 101 | } 102 | 103 | func TestMethod(t *testing.T) { 104 | mw, err := wasm.NewMiddleware(testCtx, test.BinE2EMethod) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | defer mw.Close(testCtx) 109 | 110 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 111 | if want, have := "POST", r.Method; want != have { 112 | t.Fatalf("unexpected request method, want: %q, have: %q", want, have) 113 | } 114 | body, err := io.ReadAll(r.Body) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | if want, have := "GET", string(body); want != have { 119 | t.Fatalf("unexpected request body, want: %q, have: %q", want, have) 120 | } 121 | }) 122 | 123 | ts := httptest.NewServer(mw.NewHandler(testCtx, next)) 124 | defer ts.Close() 125 | 126 | resp, err := ts.Client().Get(ts.URL) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | defer resp.Body.Close() 131 | } 132 | 133 | func TestURI(t *testing.T) { 134 | mw, err := wasm.NewMiddleware(testCtx, test.BinE2EURI) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | defer mw.Close(testCtx) 139 | 140 | var ts *httptest.Server 141 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 142 | if want, have := "/v1.0/hello?name=teddy", r.URL.RequestURI(); want != have { 143 | t.Fatalf("unexpected request URI, want: %q, have: %q", want, have) 144 | } 145 | body, err := io.ReadAll(r.Body) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | if want, have := "/v1.0/hi?name=panda", string(body); want != have { 150 | t.Fatalf("unexpected request body, want: %q, have: %q", want, have) 151 | } 152 | 153 | w.Write([]byte(responseBody)) 154 | }) 155 | 156 | ts = httptest.NewServer(mw.NewHandler(testCtx, next)) 157 | defer ts.Close() 158 | 159 | resp, err := ts.Client().Get(ts.URL + "/v1.0/hi?name=panda") 160 | if err != nil { 161 | t.Fatal(err) 162 | } 163 | defer resp.Body.Close() 164 | body, err := io.ReadAll(resp.Body) 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | if want, have := responseBody, string(body); want != have { 169 | t.Fatalf("unexpected response body, want: %q, have: %q", want, have) 170 | } 171 | } 172 | 173 | // TestHeaderNames uses test.BinE2EHeaderNames which ensures count/len are 174 | // correct. 175 | func TestHeaderNames(t *testing.T) { 176 | mw, err := wasm.NewMiddleware(testCtx, test.BinE2EHeaderNames) 177 | if err != nil { 178 | t.Fatal(err) 179 | } 180 | defer mw.Close(testCtx) 181 | 182 | ts := httptest.NewServer(mw.NewHandler(testCtx, noopHandler)) 183 | defer ts.Close() 184 | 185 | resp, err := ts.Client().Get(ts.URL) 186 | if err != nil { 187 | t.Fatal(err) 188 | } 189 | defer resp.Body.Close() 190 | if resp.StatusCode != 200 { 191 | t.Fatalf("invalid status code: %d, status message: %s", resp.StatusCode, resp.Status) 192 | } 193 | } 194 | 195 | func TestHeaderValue(t *testing.T) { 196 | mw, err := wasm.NewMiddleware(testCtx, test.BinE2EHeaderValue) 197 | if err != nil { 198 | t.Fatal(err) 199 | } 200 | defer mw.Close(testCtx) 201 | 202 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 203 | if want, have := "text/plain", r.Header.Get("Content-Type"); want != have { 204 | t.Fatalf("unexpected Content-Type, want: %q, have: %q", want, have) 205 | } 206 | }) 207 | 208 | ts := httptest.NewServer(mw.NewHandler(testCtx, next)) 209 | defer ts.Close() 210 | 211 | resp, err := ts.Client().Get(ts.URL) 212 | if err != nil { 213 | t.Fatal(err) 214 | } 215 | defer resp.Body.Close() 216 | } 217 | 218 | // TestHandleResponse uses test.BinE2EHandleResponse which ensures reqCtx 219 | // propagates from handler.FuncHandleRequest to handler.FuncHandleResponse. 220 | func TestHandleResponse(t *testing.T) { 221 | mw, err := wasm.NewMiddleware(testCtx, test.BinE2EHandleResponse) 222 | if err != nil { 223 | t.Fatal(err) 224 | } 225 | defer mw.Close(testCtx) 226 | 227 | ts := httptest.NewServer(mw.NewHandler(testCtx, noopHandler)) 228 | defer ts.Close() 229 | 230 | resp, err := ts.Client().Get(ts.URL) 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | defer resp.Body.Close() 235 | if resp.StatusCode != 200 { 236 | t.Fatalf("invalid status code: %d, status message: %s", resp.StatusCode, resp.Status) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /handler/nethttp/tck_test.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "context" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/http-wasm/http-wasm-host-go/tck" 9 | ) 10 | 11 | func TestTCK(t *testing.T) { 12 | // Initialize the TCK guest wasm module. 13 | mw, err := NewMiddleware(context.Background(), tck.GuestWASM) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | // Set the delegate handler of the middleware to the backend. 19 | h := mw.NewHandler(context.Background(), tck.BackendHandler()) 20 | 21 | tests := []struct { 22 | http2 bool 23 | expected string 24 | }{ 25 | { 26 | http2: false, 27 | expected: "HTTP/1.1", 28 | }, 29 | { 30 | http2: true, 31 | expected: "HTTP/2.0", 32 | }, 33 | } 34 | 35 | for _, tt := range tests { 36 | tc := tt 37 | t.Run(tc.expected, func(t *testing.T) { 38 | // Start the server. 39 | ts := httptest.NewUnstartedServer(h) 40 | if tc.http2 { 41 | ts.EnableHTTP2 = true 42 | ts.StartTLS() 43 | } else { 44 | ts.Start() 45 | } 46 | defer ts.Close() 47 | 48 | // Run tests, issuing HTTP requests to server. 49 | tck.Run(t, ts.Client(), ts.URL) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /handler/options.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tetratelabs/wazero" 7 | 8 | "github.com/http-wasm/http-wasm-host-go/api" 9 | ) 10 | 11 | // Option is configuration for NewMiddleware 12 | type Option func(*options) 13 | 14 | // NewRuntime returns a new wazero runtime which is called when creating a new 15 | // middleware instance, which also closes it. 16 | type NewRuntime func(context.Context) (wazero.Runtime, error) 17 | 18 | // Runtime provides the wazero.Runtime and defaults to wazero.NewRuntime. 19 | func Runtime(newRuntime NewRuntime) Option { 20 | return func(h *options) { 21 | h.newRuntime = newRuntime 22 | } 23 | } 24 | 25 | // GuestConfig is the configuration used to instantiate the guest. 26 | func GuestConfig(guestConfig []byte) Option { 27 | return func(h *options) { 28 | h.guestConfig = guestConfig 29 | } 30 | } 31 | 32 | // ModuleConfig is the configuration used to instantiate the guest. 33 | func ModuleConfig(moduleConfig wazero.ModuleConfig) Option { 34 | return func(h *options) { 35 | h.moduleConfig = moduleConfig 36 | } 37 | } 38 | 39 | // Logger sets the logger used by the guest when it calls "log". Defaults to 40 | // api.NoopLogger. 41 | func Logger(logger api.Logger) Option { 42 | return func(h *options) { 43 | h.logger = logger 44 | } 45 | } 46 | 47 | type options struct { 48 | newRuntime func(context.Context) (wazero.Runtime, error) 49 | guestConfig []byte 50 | moduleConfig wazero.ModuleConfig 51 | logger api.Logger 52 | } 53 | 54 | // DefaultRuntime implements options.newRuntime. 55 | func DefaultRuntime(ctx context.Context) (wazero.Runtime, error) { 56 | return wazero.NewRuntime(ctx), nil 57 | } 58 | -------------------------------------------------------------------------------- /handler/state.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/http-wasm/http-wasm-host-go/api/handler" 9 | ) 10 | 11 | // requestStateKey is a context.Context value associated with a requestState 12 | // pointer to the current request. 13 | type requestStateKey struct{} 14 | 15 | func requestStateFromContext(ctx context.Context) *requestState { 16 | return ctx.Value(requestStateKey{}).(*requestState) 17 | } 18 | 19 | type requestState struct { 20 | afterNext bool 21 | requestBodyReader io.ReadCloser 22 | requestBodyWriter io.Writer 23 | responseBodyReader io.ReadCloser 24 | responseBodyWriter io.Writer 25 | 26 | // features are the current request's features which may be more than 27 | // Middleware.Features. 28 | features handler.Features 29 | 30 | putPool func(x any) 31 | g *guest 32 | } 33 | 34 | func (r *requestState) closeRequest() (err error) { 35 | if reqBW := r.requestBodyWriter; reqBW != nil { 36 | if f, ok := reqBW.(http.Flusher); ok { 37 | f.Flush() 38 | } 39 | r.requestBodyWriter = nil 40 | } 41 | if reqBR := r.requestBodyReader; reqBR != nil { 42 | err = reqBR.Close() 43 | r.requestBodyReader = nil 44 | } 45 | return 46 | } 47 | 48 | // Close releases all resources for the current request, including: 49 | // - putting the guest module back into the pool 50 | // - releasing any request body resources 51 | // - releasing any response body resources 52 | func (r *requestState) Close() (err error) { 53 | if g := r.g; g != nil { 54 | r.putPool(r.g) 55 | r.g = nil 56 | } 57 | err = r.closeRequest() 58 | if respBW := r.responseBodyWriter; respBW != nil { 59 | if f, ok := respBW.(http.Flusher); ok { 60 | f.Flush() 61 | } 62 | r.responseBodyWriter = nil 63 | } 64 | if respBR := r.responseBodyReader; respBR != nil { 65 | err = respBR.Close() 66 | r.responseBodyReader = nil 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /internal/test/testdata.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | _ "embed" 5 | "log" 6 | "os" 7 | "path" 8 | "runtime" 9 | ) 10 | 11 | //go:embed testdata/bench/log.wasm 12 | var BinBenchLog []byte 13 | 14 | //go:embed testdata/bench/get_uri.wasm 15 | var BinBenchGetURI []byte 16 | 17 | //go:embed testdata/bench/set_uri.wasm 18 | var BinBenchSetURI []byte 19 | 20 | //go:embed testdata/bench/get_header_names.wasm 21 | var BinBenchGetHeaderNames []byte 22 | 23 | //go:embed testdata/bench/get_header_values.wasm 24 | var BinBenchGetHeaderValues []byte 25 | 26 | //go:embed testdata/bench/set_header_value.wasm 27 | var BinBenchSetHeaderValue []byte 28 | 29 | //go:embed testdata/bench/add_header_value.wasm 30 | var BinBenchAddHeaderValue []byte 31 | 32 | //go:embed testdata/bench/remove_header.wasm 33 | var BinBenchRemoveHeader []byte 34 | 35 | //go:embed testdata/bench/read_body.wasm 36 | var BinBenchReadBody []byte 37 | 38 | //go:embed testdata/bench/read_body_stream.wasm 39 | var BinBenchReadBodyStream []byte 40 | 41 | //go:embed testdata/bench/write_body.wasm 42 | var BinBenchWriteBody []byte 43 | 44 | //go:embed testdata/bench/set_status_code.wasm 45 | var BinBenchSetStatusCode []byte 46 | 47 | var BinExampleAuth = func() []byte { 48 | return binExample("auth") 49 | }() 50 | 51 | var BinExampleConfig = func() []byte { 52 | return binExample("config") 53 | }() 54 | 55 | var BinExampleLog = func() []byte { 56 | return binExample("log") 57 | }() 58 | 59 | var BinExampleRedact = func() []byte { 60 | return binExample("redact") 61 | }() 62 | 63 | var BinExampleRouter = func() []byte { 64 | return binExample("router") 65 | }() 66 | 67 | var BinExampleWASI = func() []byte { 68 | return binExample("wasi") 69 | }() 70 | 71 | //go:embed testdata/e2e/protocol_version.wasm 72 | var BinE2EProtocolVersion []byte 73 | 74 | //go:embed testdata/e2e/method.wasm 75 | var BinE2EMethod []byte 76 | 77 | //go:embed testdata/e2e/uri.wasm 78 | var BinE2EURI []byte 79 | 80 | //go:embed testdata/e2e/header_value.wasm 81 | var BinE2EHeaderValue []byte 82 | 83 | //go:embed testdata/e2e/handle_response.wasm 84 | var BinE2EHandleResponse []byte 85 | 86 | //go:embed testdata/e2e/header_names.wasm 87 | var BinE2EHeaderNames []byte 88 | 89 | //go:embed testdata/error/panic_on_handle_request.wasm 90 | var BinErrorPanicOnHandleRequest []byte 91 | 92 | //go:embed testdata/error/panic_on_handle_response.wasm 93 | var BinErrorPanicOnHandleResponse []byte 94 | 95 | //go:embed testdata/error/panic_on_start.wasm 96 | var BinErrorPanicOnStart []byte 97 | 98 | //go:embed testdata/error/set_request_header_after_next.wasm 99 | var BinErrorSetRequestHeaderAfterNext []byte 100 | 101 | // binExample instead of go:embed as files aren't relative to this directory. 102 | func binExample(name string) []byte { 103 | _, thisFile, _, ok := runtime.Caller(1) 104 | if !ok { 105 | log.Panicln("cannot determine current path") 106 | } 107 | p := path.Join(path.Dir(thisFile), "..", "..", "examples", name+".wasm") 108 | if wasm, err := os.ReadFile(p); err != nil { 109 | log.Panicln(err) 110 | return nil 111 | } else { 112 | return wasm 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/add_header_value.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/bench/add_header_value.wasm -------------------------------------------------------------------------------- /internal/test/testdata/bench/add_header_value.wat: -------------------------------------------------------------------------------- 1 | (module $add_header_value 2 | 3 | (import "http_handler" "add_header_value" (func $add_header_value 4 | (param $kind i32) 5 | (param $name i32) (param $name_len i32) 6 | (param $value i32) (param $value_len i32))) 7 | 8 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 9 | 10 | (global $name i32 (i32.const 0)) 11 | (data (i32.const 0) "Set-Cookie") 12 | (global $name_len i32 (i32.const 10)) 13 | 14 | (global $value i32 (i32.const 16)) 15 | (data (i32.const 16) "a=b") 16 | (global $value_len i32 (i32.const 3)) 17 | 18 | (func (export "handle_request") (result (; ctx_next ;) i64) 19 | (call $add_header_value 20 | (i32.const 1) ;; header_kind_response 21 | (global.get $name) (global.get $name_len) 22 | (global.get $value) (global.get $value_len)) 23 | 24 | ;; skip any next handler as the benchmark is about add_header_value. 25 | (return (i64.const 0))) 26 | 27 | ;; handle_response should not be called as handle_request returns zero. 28 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 29 | (unreachable)) 30 | ) 31 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/get_header_names.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/bench/get_header_names.wasm -------------------------------------------------------------------------------- /internal/test/testdata/bench/get_header_names.wat: -------------------------------------------------------------------------------- 1 | (module $get_header_names 2 | 3 | (import "http_handler" "get_header_names" (func $get_header_names 4 | (param $kind i32) 5 | (param $buf i32) (param $buf_limit i32) 6 | (result (; count << 32| len ;) i64))) 7 | 8 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 9 | 10 | (global $buf i32 (i32.const 64)) 11 | (global $buf_limit i32 (i32.const 64)) 12 | 13 | (func (export "handle_request") (result (; ctx_next ;) i64) 14 | (call $get_header_names 15 | (i32.const 0) ;; header_kind_request 16 | (global.get $buf) (global.get $buf_limit)) 17 | (drop) 18 | 19 | ;; skip any next handler as the benchmark is about get_header_names. 20 | (return (i64.const 0))) 21 | 22 | ;; handle_response should not be called as handle_request returns zero. 23 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 24 | (unreachable)) 25 | ) 26 | 27 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/get_header_values.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/bench/get_header_values.wasm -------------------------------------------------------------------------------- /internal/test/testdata/bench/get_header_values.wat: -------------------------------------------------------------------------------- 1 | (module $get_header_values 2 | 3 | (import "http_handler" "get_header_values" (func $get_header_values 4 | (param $kind i32) 5 | (param $name i32) (param $name_len i32) 6 | (param $buf i32) (param $buf_limit i32) 7 | (result (; count << 32| len ;) i64))) 8 | 9 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 10 | 11 | (global $name i32 (i32.const 0)) 12 | (data (i32.const 0) "Accept") 13 | (global $name_len i32 (i32.const 6)) 14 | 15 | (global $buf i32 (i32.const 64)) 16 | (global $buf_limit i32 (i32.const 64)) 17 | 18 | (func (export "handle_request") (result (; ctx_next ;) i64) 19 | (call $get_header_values 20 | (i32.const 0) ;; header_kind_request 21 | (global.get $name) (global.get $name_len) 22 | (global.get $buf) (global.get $buf_limit)) 23 | (drop) 24 | 25 | ;; skip any next handler as the benchmark is about get_header_values. 26 | (return (i64.const 0))) 27 | 28 | ;; handle_response should not be called as handle_request returns zero. 29 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 30 | (unreachable)) 31 | ) 32 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/get_uri.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/bench/get_uri.wasm -------------------------------------------------------------------------------- /internal/test/testdata/bench/get_uri.wat: -------------------------------------------------------------------------------- 1 | (module $get_uri 2 | 3 | (import "http_handler" "get_uri" 4 | (func $get_uri (param i32 i32) (result i32))) 5 | 6 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 7 | (global $buf i32 (i32.const 0)) 8 | (global $buf_limit i32 (i32.const 64)) 9 | 10 | (func (export "handle_request") (result (; ctx_next ;) i64) 11 | (call $get_uri 12 | (global.get $buf) 13 | (global.get $buf_limit)) 14 | (drop) 15 | 16 | ;; skip any next handler as the benchmark is about get_uri. 17 | (return (i64.const 0))) 18 | 19 | ;; handle_response should not be called as handle_request returns zero. 20 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 21 | (unreachable)) 22 | ) 23 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/log.wasm: -------------------------------------------------------------------------------- 1 | asm``~` http_handlerlog A A -memoryhandle_requesthandle_response 2 |  A##B  A hello world^nameloglog0levelbuf buf_limitreqCtxis_errormessage message_len -------------------------------------------------------------------------------- /internal/test/testdata/bench/log.wat: -------------------------------------------------------------------------------- 1 | (module $log 2 | 3 | (import "http_handler" "log" (func $log 4 | (param $level i32) 5 | (param $buf i32) (param $buf_limit i32))) 6 | 7 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 8 | (global $message i32 (i32.const 0)) 9 | (data (i32.const 0) "hello world") 10 | (global $message_len i32 (i32.const 11)) 11 | 12 | (func (export "handle_request") (result (; ctx_next ;) i64) 13 | (call $log 14 | (i32.const 0) ;; log_level_info 15 | (global.get $message) 16 | (global.get $message_len)) 17 | 18 | ;; skip any next handler as the benchmark is about log. 19 | (return (i64.const 0))) 20 | 21 | ;; handle_response should not be called as handle_request returns zero. 22 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 23 | (unreachable)) 24 | ) 25 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/read_body.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/bench/read_body.wasm -------------------------------------------------------------------------------- /internal/test/testdata/bench/read_body.wat: -------------------------------------------------------------------------------- 1 | (module $read_body 2 | 3 | (import "http_handler" "read_body" (func $read_body 4 | (param $kind i32) 5 | (param $buf i32) (param $buf_len i32) 6 | (result (; 0 or EOF(1) << 32 | len ;) i64))) 7 | 8 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 9 | 10 | ;; eof is the upper 32-bits of the $read_body result on EOF. 11 | (global $eof i64 (i64.const 4294967296)) ;; `1<<32|0` 12 | 13 | (func (export "handle_request") (result (; ctx_next ;) i64) 14 | (local $result i64) 15 | 16 | ;; read up to 2KB into memory 17 | (local.set $result 18 | (call $read_body 19 | (i32.const 0) ;; body_kind_request 20 | (i32.const 0) (i32.const 2048))) 21 | 22 | ;; if result & eof != eof { panic } 23 | (if (i64.ne 24 | (i64.and (local.get $result) (global.get $eof)) 25 | (global.get $eof)) 26 | (then unreachable)) ;; fail as we couldn't buffer the whole response. 27 | 28 | ;; if len == 0 { panic } 29 | (if (i32.eqz (i32.wrap_i64 (local.get $result))) 30 | (then (unreachable))) ;; didn't read the body 31 | 32 | ;; skip any next handler as the benchmark is about read_body. 33 | (return (i64.const 0))) 34 | 35 | ;; handle_response should not be called as handle_request returns zero. 36 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 37 | (unreachable)) 38 | ) 39 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/read_body_stream.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/bench/read_body_stream.wasm -------------------------------------------------------------------------------- /internal/test/testdata/bench/read_body_stream.wat: -------------------------------------------------------------------------------- 1 | (module $read_body_stream 2 | 3 | (import "http_handler" "read_body" (func $read_body 4 | (param $kind i32) 5 | (param $buf i32) (param $buf_len i32) 6 | (result (; 0 or EOF(1) << 32 | len ;) i64))) 7 | 8 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 9 | 10 | ;; eof is the upper 32-bits of the $read_body result on EOF. 11 | (global $eof i64 (i64.const 4294967296)) ;; `1<<32|0` 12 | 13 | (func (export "handle_request") (result (; ctx_next ;) i64) 14 | (local $result i64) 15 | 16 | (loop $not_eof 17 | ;; read up to 2KB into memory 18 | (local.set $result 19 | (call $read_body 20 | (i32.const 0) ;; body_kind_request 21 | (i32.const 0) (i32.const 2048))) 22 | 23 | ;; if result & eof != eof { continue } else { break } 24 | (br_if $not_eof (i64.ne 25 | (i64.and (local.get $result) (global.get $eof)) 26 | (global.get $eof)))) 27 | 28 | ;; skip any next handler as the benchmark is about read_body. 29 | (return (i64.const 0))) 30 | 31 | ;; handle_response should not be called as handle_request returns zero. 32 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 33 | (unreachable)) 34 | ) 35 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/remove_header.wasm: -------------------------------------------------------------------------------- 1 | asm``~` http_handler remove_header A A 2 | -memoryhandle_requesthandle_response 3 |  A##B  A 4 | Set-CookieUname remove_header remove_headerreqCtxis_errornamename_len -------------------------------------------------------------------------------- /internal/test/testdata/bench/remove_header.wat: -------------------------------------------------------------------------------- 1 | (module $remove_header 2 | (import "http_handler" "remove_header" 3 | (func $remove_header (param i32 i32 i32))) 4 | 5 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 6 | 7 | (global $name i32 (i32.const 0)) 8 | (data (i32.const 0) "Set-Cookie") 9 | (global $name_len i32 (i32.const 10)) 10 | 11 | (func (export "handle_request") (result (; ctx_next ;) i64) 12 | (call $remove_header 13 | (i32.const 1) ;; header_kind_response 14 | (global.get $name) (global.get $name_len)) 15 | 16 | ;; skip any next handler as the benchmark is about remove_header. 17 | (return (i64.const 0))) 18 | 19 | ;; handle_response should not be called as handle_request returns zero. 20 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 21 | (unreachable)) 22 | ) 23 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/set_header_value.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/bench/set_header_value.wasm -------------------------------------------------------------------------------- /internal/test/testdata/bench/set_header_value.wat: -------------------------------------------------------------------------------- 1 | (module $set_header_value 2 | 3 | (import "http_handler" "set_header_value" (func $set_header_value 4 | (param $kind i32) 5 | (param $name i32) (param $name_len i32) 6 | (param $value i32) (param $value_len i32))) 7 | 8 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 9 | 10 | (global $name i32 (i32.const 0)) 11 | (data (i32.const 0) "Content-Type") 12 | (global $name_len i32 (i32.const 12)) 13 | 14 | (global $value i32 (i32.const 16)) 15 | (data (i32.const 16) "text/plain") 16 | (global $value_len i32 (i32.const 10)) 17 | 18 | (func (export "handle_request") (result (; ctx_next ;) i64) 19 | (call $set_header_value 20 | (i32.const 1) ;; header_kind_response 21 | (global.get $name) (global.get $name_len) 22 | (global.get $value) (global.get $value_len)) 23 | 24 | ;; skip any next handler as the benchmark is about set_header_value. 25 | (return (i64.const 0))) 26 | 27 | ;; handle_response should not be called as handle_request returns zero. 28 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 29 | (unreachable)) 30 | ) 31 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/set_status_code.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/bench/set_status_code.wasm -------------------------------------------------------------------------------- /internal/test/testdata/bench/set_status_code.wat: -------------------------------------------------------------------------------- 1 | (module $set_status_code 2 | (import "http_handler" "set_status_code" 3 | (func $set_status_code (param i32))) 4 | 5 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 6 | 7 | (func (export "handle_request") (result (; ctx_next ;) i64) 8 | (call $set_status_code (i32.const 404)) 9 | 10 | ;; skip any next handler as the benchmark is about set_status_code. 11 | (return (i64.const 0))) 12 | 13 | ;; handle_response should not be called as handle_request returns zero. 14 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 15 | (unreachable)) 16 | ) 17 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/set_uri.wasm: -------------------------------------------------------------------------------- 1 | asm 2 | ``~ http_handlerset_uri A A -memoryhandle_requesthandle_response 3 |  ##B  A /v1.0/hello?name=teddyGnameset_uri 4 | set_urireqCtxis_erroruriuri_len -------------------------------------------------------------------------------- /internal/test/testdata/bench/set_uri.wat: -------------------------------------------------------------------------------- 1 | (module $set_uri 2 | (import "http_handler" "set_uri" 3 | (func $set_uri (param i32 i32))) 4 | 5 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 6 | (global $uri i32 (i32.const 0)) 7 | (data (i32.const 0) "/v1.0/hello?name=teddy") 8 | (global $uri_len i32 (i32.const 22)) 9 | 10 | (func (export "handle_request") (result (; ctx_next ;) i64) 11 | (call $set_uri 12 | (global.get $uri) 13 | (global.get $uri_len)) 14 | 15 | ;; skip any next handler as the benchmark is about set_uri. 16 | (return (i64.const 0))) 17 | 18 | ;; handle_response should not be called as handle_request returns zero. 19 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 20 | (unreachable)) 21 | ) 22 | -------------------------------------------------------------------------------- /internal/test/testdata/bench/write_body.wasm: -------------------------------------------------------------------------------- 1 | asm``~` http_handler 2 | write_body A A -memoryhandle_requesthandle_response 3 |  A##B  A hello worldcname 4 | write_body  5 | write_body-kindbufbuf_lenreqCtxis_errorbodybody_len -------------------------------------------------------------------------------- /internal/test/testdata/bench/write_body.wat: -------------------------------------------------------------------------------- 1 | (module $write_body 2 | 3 | (import "http_handler" "write_body" (func $write_body 4 | (param $kind i32) 5 | (param $buf i32) (param $buf_len i32))) 6 | 7 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 8 | 9 | (global $body i32 (i32.const 0)) 10 | (data (i32.const 16) "hello world") 11 | (global $body_len i32 (i32.const 11)) 12 | 13 | (func (export "handle_request") (result (; ctx_next ;) i64) 14 | (call $write_body 15 | (i32.const 1) ;; body_kind_response 16 | (global.get $body) (global.get $body_len)) 17 | 18 | ;; skip any next handler as the benchmark is about write_body. 19 | (return (i64.const 0))) 20 | 21 | ;; handle_response should not be called as handle_request returns zero. 22 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 23 | (unreachable)) 24 | ) 25 | -------------------------------------------------------------------------------- /internal/test/testdata/e2e/handle_response.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/e2e/handle_response.wasm -------------------------------------------------------------------------------- /internal/test/testdata/e2e/handle_response.wat: -------------------------------------------------------------------------------- 1 | (module $handle_response 2 | 3 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 4 | 5 | ;; reqCtx is the upper 32-bits of the $ctx_next result the host should 6 | ;; propagate from handle_request to handle_response. 7 | (global $reqCtx (export "reqCtx") (mut i32) (i32.const 42)) 8 | 9 | ;; handle_request sets the request ID to the global then increments the 10 | ;; global. 11 | (func (export "handle_request") (result (; ctx_next ;) i64) 12 | (local $reqCtx i32) 13 | 14 | ;; reqCtx := global.reqCtx 15 | (local.set $reqCtx (global.get $reqCtx)) 16 | 17 | ;; global.reqCtx++ 18 | (global.set $reqCtx (i32.add (global.get $reqCtx) (i32.const 1))) 19 | 20 | ;; return uint64(reqCtx) << 32 | uint64(1) 21 | (return 22 | (i64.or 23 | (i64.shl (i64.extend_i32_u (local.get $reqCtx)) (i64.const 32)) 24 | (i64.const 1)))) 25 | 26 | ;; If propagation works, the current request ID should be one less than the 27 | ;; global. 28 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) 29 | ;; if reqCtx != global.reqCtx - 1 { panic } 30 | (if (i32.ne 31 | (local.get $reqCtx) 32 | (i32.sub (global.get $reqCtx) (i32.const 1))) 33 | (then unreachable))) ;; fail as the host didn't propagate the reqCtx 34 | ) 35 | -------------------------------------------------------------------------------- /internal/test/testdata/e2e/header_names.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/e2e/header_names.wasm -------------------------------------------------------------------------------- /internal/test/testdata/e2e/header_names.wat: -------------------------------------------------------------------------------- 1 | (module $get_header_names 2 | 3 | (import "http_handler" "get_header_names" (func $get_header_names 4 | (param $kind i32) 5 | (param $buf i32) (param $buf_limit i32) 6 | (result (; count << 32| len ;) i64))) 7 | 8 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 9 | 10 | (func (export "handle_request") (result (; ctx_next ;) i64) 11 | (local $buf i32) 12 | (local $result i64) 13 | (local $len i32) 14 | (local $expected_count i32) 15 | (local $count i32) 16 | 17 | ;; read up to 2KB into memory 18 | (local.set $result 19 | (call $get_header_names 20 | (i32.const 0) ;; header_kind_request 21 | (local.get $buf) (i32.const 2048))) 22 | 23 | ;; if result == 0 { return } 24 | (if (i64.eqz (local.get $result)) 25 | (then (return (i64.const 0)))) ;; no headers 26 | 27 | ;; expected_count = uint32(result >> 32) 28 | (local.set $expected_count 29 | (i32.wrap_i64 (i64.shr_u (local.get $result) (i64.const 32)))) 30 | 31 | ;; len = uint32(result) 32 | (local.set $len (i32.wrap_i64 (local.get $result))) 33 | 34 | ;; if $len > 2048 { retry } 35 | (if (i32.gt_u (local.get $len) (i32.const 2048)) 36 | (then 37 | (drop (call $get_header_names 38 | (i32.const 0) ;; header_kind_request 39 | (local.get $buf) (local.get $len))))) 40 | 41 | ;; loop while we can read a NUL-terminated name. 42 | (loop $names 43 | ;; if mem[buf] == NUL 44 | (if (i32.eqz (i32.load8_u (local.get $buf))) 45 | (then ;; reached the end of the name 46 | ;; count++ 47 | (local.set $count (i32.add (local.get $count) (i32.const 1))))) 48 | 49 | (local.set $buf (i32.add (local.get $buf) (i32.const 1))) ;; buf++ 50 | (local.set $len (i32.sub (local.get $len) (i32.const 1))) ;; len-- 51 | 52 | ;; if len > 0 { continue } else { break } 53 | (br_if $names (i32.gt_u (local.get $len) (i32.const 0)))) 54 | 55 | ;; if count != expected_count { panic } 56 | (if (i32.ne (local.get $count) (local.get $expected_count)) 57 | (then (unreachable))) ;; the result wasn't NUL-terminated 58 | 59 | ;; uint32(ctx_next) == 1 means proceed to the next handler on the host. 60 | (return (i64.const 1))) 61 | 62 | ;; handle_response is no-op as this is a request-only handler. 63 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 64 | ) 65 | -------------------------------------------------------------------------------- /internal/test/testdata/e2e/header_value.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/e2e/header_value.wasm -------------------------------------------------------------------------------- /internal/test/testdata/e2e/header_value.wat: -------------------------------------------------------------------------------- 1 | (module $header_value 2 | (import "http_handler" "set_header_value" (func $set_header_value 3 | (param $kind i32) 4 | (param $name i32) (param $name_len i32) 5 | (param $value i32) (param $value_len i32))) 6 | 7 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 8 | 9 | (global $name i32 (i32.const 0)) 10 | (data (i32.const 0) "Content-Type") 11 | (global $name_len i32 (i32.const 12)) 12 | 13 | (global $value i32 (i32.const 16)) 14 | (data (i32.const 16) "text/plain") 15 | (global $value_len i32 (i32.const 10)) 16 | 17 | ;; handle_request sets the "Content-Type" to "text/plain". Then, it returns 18 | ;; non-zero to proceed to the next handler. 19 | (func (export "handle_request") (result (; ctx_next ;) i64) 20 | 21 | (call $set_header_value 22 | (i32.const 0) ;; header_kind_request 23 | (global.get $name) (global.get $name_len) 24 | (global.get $value) (global.get $value_len)) 25 | 26 | ;; call the next handler 27 | (return (i64.const 1))) 28 | 29 | ;; handle_response is no-op as this is a request-only handler. 30 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 31 | ) 32 | -------------------------------------------------------------------------------- /internal/test/testdata/e2e/method.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/e2e/method.wasm -------------------------------------------------------------------------------- /internal/test/testdata/e2e/method.wat: -------------------------------------------------------------------------------- 1 | (module $method 2 | 3 | (import "http_handler" "get_method" (func $get_method 4 | (param $buf i32) (param $buf_limit i32) 5 | (result (; len ;) i32))) 6 | 7 | (import "http_handler" "set_method" (func $set_method 8 | (param $method i32) (param $method_len i32))) 9 | 10 | (import "http_handler" "write_body" (func $write_body 11 | (param $kind i32) 12 | (param $buf i32) (param $buf_len i32))) 13 | 14 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 15 | 16 | (global $post i32 (i32.const 0)) 17 | (data (i32.const 0) "POST") 18 | (global $post_len i32 (i32.const 4)) 19 | 20 | (global $buf i32 (i32.const 1024)) 21 | 22 | ;; handle_request changes the method to POST with the original method as the 23 | ;; request body. Then, it returns non-zero to proceed to the next handler. 24 | (func (export "handle_request") (result (; ctx_next ;) i64) 25 | (local $len i32) 26 | 27 | ;; read the method into memory at offset zero. 28 | (local.set $len 29 | (call $get_method (global.get $buf) (i32.const 1024))) 30 | 31 | ;; change the method to POST 32 | (call $set_method (global.get $post) (global.get $post_len)) 33 | 34 | ;; write the method to the request body. 35 | (call $write_body 36 | (i32.const 0) ;; body_kind_request 37 | (global.get $buf) (local.get $len)) 38 | 39 | ;; call the next handler 40 | (return (i64.const 1))) 41 | 42 | ;; handle_response is no-op as this is a request-only handler. 43 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 44 | ) 45 | -------------------------------------------------------------------------------- /internal/test/testdata/e2e/protocol_version.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/e2e/protocol_version.wasm -------------------------------------------------------------------------------- /internal/test/testdata/e2e/protocol_version.wat: -------------------------------------------------------------------------------- 1 | (module $protocol_version 2 | 3 | (import "http_handler" "get_protocol_version" (func $get_protocol_version 4 | (param $buf i32) (param $buf_limit i32) 5 | (result (; len ;) i32))) 6 | 7 | (import "http_handler" "write_body" (func $write_body 8 | (param $kind i32) 9 | (param $buf i32) (param $buf_len i32))) 10 | 11 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 12 | 13 | ;; handle_request writes the protocol version to the response body. 14 | (func (export "handle_request") (result (; ctx_next ;) i64) 15 | (local $len i32) 16 | 17 | ;; read the protocol version into memory at offset zero. 18 | (local.set $len 19 | (call $get_protocol_version (i32.const 0) (i32.const 1024))) 20 | 21 | ;; write the protocol version to the response body. 22 | (call $write_body 23 | (i32.const 1) ;; body_kind_response 24 | (i32.const 0) (local.get $len)) 25 | 26 | ;; skip any next handler as we wrote the response body. 27 | (return (i64.const 0))) 28 | 29 | ;; handle_response is no-op as this is a request-only handler. 30 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 31 | ) 32 | -------------------------------------------------------------------------------- /internal/test/testdata/e2e/uri.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/e2e/uri.wasm -------------------------------------------------------------------------------- /internal/test/testdata/e2e/uri.wat: -------------------------------------------------------------------------------- 1 | (module $uri 2 | 3 | (import "http_handler" "get_uri" (func $get_uri 4 | (param $buf i32) (param $buf_limit i32) 5 | (result (; len ;) i32))) 6 | 7 | (import "http_handler" "set_uri" (func $set_uri 8 | (param $uri i32) (param $uri_len i32))) 9 | 10 | (import "http_handler" "write_body" (func $write_body 11 | (param $kind i32) 12 | (param $buf i32) (param $buf_len i32))) 13 | 14 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 15 | 16 | (global $uri i32 (i32.const 0)) 17 | (data (i32.const 0) "/v1.0/hello?name=teddy") 18 | (global $uri_len i32 (i32.const 22)) 19 | 20 | (global $buf i32 (i32.const 1024)) 21 | 22 | ;; handle_request changes the uri to with the original uri as the request 23 | ;; body. Then, it returns non-zero to proceed to the next handler. 24 | (func (export "handle_request") (result (; ctx_next ;) i64) 25 | (local $len i32) 26 | 27 | ;; read the uri into memory at offset zero. 28 | (local.set $len 29 | (call $get_uri (global.get $buf) (i32.const 1024))) 30 | 31 | ;; delete the URI, which tests setting to "" doesn't crash. 32 | (call $set_uri (global.get $uri) (i32.const 0)) 33 | 34 | ;; change the uri 35 | (call $set_uri (global.get $uri) (global.get $uri_len)) 36 | 37 | ;; write the uri to the request body. 38 | (call $write_body 39 | (i32.const 0) ;; body_kind_request 40 | (global.get $buf) (local.get $len)) 41 | 42 | ;; execute test case handler to verify assertions 43 | (return (i64.const 1))) 44 | 45 | ;; handle_response is no-op as this is a request-only handler. 46 | (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 47 | ) 48 | -------------------------------------------------------------------------------- /internal/test/testdata/error/panic_on_handle_request.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/error/panic_on_handle_request.wasm -------------------------------------------------------------------------------- /internal/test/testdata/error/panic_on_handle_request.wat: -------------------------------------------------------------------------------- 1 | ;; panic_on_handle_request issues an unreachable instruction after writing 2 | ;; an error to stdout. This simulates a panic in TinyGo. 3 | (module $panic_on_handle_request 4 | ;; Import the fd_write function from wasi, used in TinyGo for println. 5 | (import "wasi_snapshot_preview1" "fd_write" 6 | (func $wasi.fd_write (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32))) 7 | 8 | ;; Allocate the minimum amount of memory, 1 page (64KB). 9 | (memory (export "memory") 1 1) 10 | 11 | ;; Pre-populate memory with the panic message, in iovec format 12 | (data (i32.const 0) "\08") ;; iovs[0].offset 13 | (data (i32.const 4) "\06") ;; iovs[0].length 14 | (data (i32.const 8) "panic!") ;; iovs[0] 15 | 16 | ;; On handle_request, write "panic!" to stdout and crash. 17 | (func $handle_request (export "handle_request") (result (; ctx_next ;) i64) 18 | ;; Write the panic to stdout via its iovec [offset, len]. 19 | (call $wasi.fd_write 20 | (i32.const 1) ;; stdout 21 | (i32.const 0) ;; where's the iovec 22 | (i32.const 1) ;; only one iovec 23 | (i32.const 0) ;; overwrite the iovec with the ignored result. 24 | ) 25 | drop ;; ignore the errno returned 26 | 27 | ;; Issue the unreachable instruction instead of returning ctx_next 28 | (unreachable)) 29 | 30 | (func $handle_response (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 31 | ) 32 | -------------------------------------------------------------------------------- /internal/test/testdata/error/panic_on_handle_response.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/error/panic_on_handle_response.wasm -------------------------------------------------------------------------------- /internal/test/testdata/error/panic_on_handle_response.wat: -------------------------------------------------------------------------------- 1 | ;; panic_on_handle_response issues an unreachable instruction after writing 2 | ;; an error to stdout. This simulates a panic in TinyGo. 3 | (module $panic_on_handle_response 4 | ;; Import the fd_write function from wasi, used in TinyGo for println. 5 | (import "wasi_snapshot_preview1" "fd_write" 6 | (func $wasi.fd_write (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32))) 7 | 8 | ;; Allocate the minimum amount of memory, 1 page (64KB). 9 | (memory (export "memory") 1 1) 10 | 11 | ;; Pre-populate memory with the panic message, in iovec format 12 | (data (i32.const 0) "\08") ;; iovs[0].offset 13 | (data (i32.const 4) "\06") ;; iovs[0].length 14 | (data (i32.const 8) "panic!") ;; iovs[0] 15 | 16 | (func $handle_request (export "handle_request") (result (; ctx_next ;) i64) 17 | (return (i64.const 1))) ;; call the next handler 18 | 19 | ;; On handle_response, write "panic!" to stdout and crash. 20 | (func $handle_response (export "handle_response") (param $reqCtx i32) (param $is_error i32) 21 | ;; Write the panic to stdout via its iovec [offset, len]. 22 | (call $wasi.fd_write 23 | (i32.const 1) ;; stdout 24 | (i32.const 0) ;; where's the iovec 25 | (i32.const 1) ;; only one iovec 26 | (i32.const 0) ;; overwrite the iovec with the ignored result. 27 | ) 28 | drop ;; ignore the errno returned 29 | 30 | ;; Issue the unreachable instruction instead of returning. 31 | (unreachable)) 32 | ) 33 | -------------------------------------------------------------------------------- /internal/test/testdata/error/panic_on_start.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/error/panic_on_start.wasm -------------------------------------------------------------------------------- /internal/test/testdata/error/panic_on_start.wat: -------------------------------------------------------------------------------- 1 | ;; panic_on_start is a WASI command which issues an unreachable instruction 2 | ;; after writing an error to stdout. This simulates a panic in TinyGo. 3 | (module $panic_on_start 4 | ;; Import the fd_write function from wasi, used in TinyGo for println. 5 | (import "wasi_snapshot_preview1" "fd_write" 6 | (func $wasi.fd_write (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32))) 7 | 8 | ;; Allocate the minimum amount of memory, 1 page (64KB). 9 | (memory (export "memory") 1 1) 10 | 11 | ;; Pre-populate memory with the panic message, in iovec format 12 | (data (i32.const 0) "\08") ;; iovs[0].offset 13 | (data (i32.const 4) "\06") ;; iovs[0].length 14 | (data (i32.const 8) "panic!") ;; iovs[0] 15 | 16 | ;; On start, write "panic!" to stdout and crash. 17 | (func $main (export "_start") 18 | ;; Write the panic to stdout via its iovec [offset, len]. 19 | (call $wasi.fd_write 20 | (i32.const 1) ;; stdout 21 | (i32.const 0) ;; where's the iovec 22 | (i32.const 1) ;; only one iovec 23 | (i32.const 0) ;; overwrite the iovec with the ignored result. 24 | ) 25 | drop ;; ignore the errno returned 26 | 27 | ;; Issue the unreachable instruction instead of exiting normally 28 | (unreachable)) 29 | 30 | ;; Export the required functions for the handler ABI 31 | (func $handle_request (export "handle_request") (result (; ctx_next ;) i64) 32 | (return (i64.const 0))) ;; don't call the next handler 33 | 34 | (func $handle_response (export "handle_response") (param $reqCtx i32) (param $is_error i32)) 35 | ) 36 | -------------------------------------------------------------------------------- /internal/test/testdata/error/set_request_header_after_next.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/internal/test/testdata/error/set_request_header_after_next.wasm -------------------------------------------------------------------------------- /internal/test/testdata/error/set_request_header_after_next.wat: -------------------------------------------------------------------------------- 1 | (module $set_request_header_after_next 2 | (import "http_handler" "set_header_value" (func $set_header_value 3 | (param $kind i32) 4 | (param $name i32) (param $name_len i32) 5 | (param $value i32) (param $value_len i32))) 6 | 7 | (memory (export "memory") 1 1 (; 1 page==64KB ;)) 8 | 9 | (global $name i32 (i32.const 0)) 10 | (data (i32.const 0) "Content-Type") 11 | (global $name_len i32 (i32.const 12)) 12 | 13 | (global $value i32 (i32.const 16)) 14 | (data (i32.const 16) "text/plain") 15 | (global $value_len i32 (i32.const 10)) 16 | 17 | ;; handle_request returns non-zero to proceed to the next handler. 18 | (func $handle_request (export "handle_request") (result (; ctx_next ;) i64) 19 | ;; call the next handler 20 | (return (i64.const 1))) 21 | 22 | ;; handle_response tries to set a request header even though it is too late. 23 | (func $handle_response (export "handle_response") (param $reqCtx i32) (param $is_error i32) 24 | (call $set_header_value 25 | (i32.const 0) ;; header_kind_request 26 | (global.get $name) (global.get $name_len) 27 | (global.get $value) (global.get $value_len))) 28 | ) 29 | -------------------------------------------------------------------------------- /tck/README.md: -------------------------------------------------------------------------------- 1 | # http-wasm technology compatibility kit (TCK) 2 | 3 | The TCK is a test suite for checking conformance of http-wasm implementations 4 | versus the http-wasm [ABI specification][1]. 5 | 6 | It consists of a guest Wasm binary and a test runner which issues HTTP requests 7 | to a given endpoint. The requests drive different test cases implemented in 8 | the Wasm binary, and the runner verifies that the resulting response from the 9 | endpoint matches expectations for that binary. 10 | 11 | ## Running the TCK 12 | 13 | The TCK is implemented using Go's standard testing framework, so it can be 14 | loaded as a package for Go hosts. A TCK [standalone runner][8] is also provided 15 | for use with Non-Go hosts, or Go hosts that would like to run the TCK outside 16 | the standard testing framework. 17 | 18 | The basic steps for running the TCK are 19 | 20 | 1. Implement the backend handler, which is the business logic wrapped by 21 | middleware 22 | 2. Set up the middleware using the TCK guest Wasm module 23 | 3. Start an HTTP server serving this middleware 24 | 4. Run the tests, pointing at the URL for the HTTP server 25 | 26 | The general flow of the HTTP request looks like the follows. Depending on the 27 | test case, the guest Wasm may or may not pass the request to the backend. 28 | 29 | ```mermaid 30 | graph LR; 31 | A[TCK Runner] --> B[TCK Guest Wasm] 32 | B[TCK Guest Wasm] -.-> C[Backend] 33 | ``` 34 | 35 | We provide an implementation of the backend handler using the Go http.Handler 36 | interface, [tck.BackendHandler][2]. 37 | 38 | If the Go host uses [http.Handler][7] or types that can be easily coerced to 39 | it, it can be simplest to initialize this handler directly as the backend 40 | being wrapped by the middleware. 41 | 42 | It is also possible to use [tck.StartBackend][3] 43 | to start an HTTP server using this backend handler. This is often the easiest 44 | method for proxy servers. 45 | 46 | Otherwise, it may be simplest to reimplement the logic in the backend handler 47 | using the Go host's framework, following the logic in `BackendHandler`. 48 | 49 | The binary for the guest wasm module is provided as [tck.GuestWASM][4]. 50 | 51 | With the HTTP server started and serving the middleware and backend, the tests 52 | can be run using [tck.Run][5]. 53 | 54 | [TestTCK][6] demonstrates a full example for the net/http middleware provided 55 | in this repository. 56 | 57 | [1]: https://http-wasm.io/http-handler-abi/ 58 | [2]: https://pkg.go.dev/github.com/http-wasm/http-wasm-host-go/tck#BackendHandler 59 | [3]: https://pkg.go.dev/github.com/http-wasm/http-wasm-host-go/tck#StartBackend 60 | [4]: https://pkg.go.dev/github.com/http-wasm/http-wasm-host-go/tck#GuestWASM 61 | [5]: https://pkg.go.dev/github.com/http-wasm/http-wasm-host-go/tck#Run 62 | [6]: ../handler/nethttp/tck_test.go 63 | [7]: https://pkg.go.dev/net/http#Handler 64 | [8]: https://github.com/http-wasm/http-wasm-tck 65 | -------------------------------------------------------------------------------- /tck/backend.go: -------------------------------------------------------------------------------- 1 | package tck 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "net/http/httptest" 8 | ) 9 | 10 | // BackendHandler is a http.Handler implementing the logic expected by the TCK. 11 | // It serves to echo back information from the request to the response for 12 | // checking expectations. 13 | func BackendHandler() http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | w.Header().Set("x-httpwasm-next-method", r.Method) 16 | w.Header().Set("x-httpwasm-next-uri", r.RequestURI) 17 | for k, vs := range r.Header { 18 | for i, v := range vs { 19 | w.Header().Add(fmt.Sprintf("x-httpwasm-next-header-%s-%d", k, i), v) 20 | } 21 | } 22 | }) 23 | } 24 | 25 | // StartBackend starts a httptest.Server at the given address implementing BackendHandler. 26 | func StartBackend(addr string) *httptest.Server { 27 | s := httptest.NewUnstartedServer(BackendHandler()) 28 | if addr != "" { 29 | s.Listener.Close() 30 | l, err := net.Listen("tcp", addr) 31 | if err != nil { 32 | panic(err) 33 | } 34 | s.Listener = l 35 | } 36 | s.Start() 37 | return s 38 | } 39 | -------------------------------------------------------------------------------- /tck/guest/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/anuraaga/http-wasm-tck/guest 2 | 3 | go 1.19 4 | 5 | require github.com/http-wasm/http-wasm-guest-tinygo v0.3.1-0.20231031134125-487a6e2eec5e 6 | -------------------------------------------------------------------------------- /tck/guest/go.sum: -------------------------------------------------------------------------------- 1 | github.com/http-wasm/http-wasm-guest-tinygo v0.3.1-0.20231031134125-487a6e2eec5e h1:mUEuIuH+XAtp5tkJPpwN5ZAtgvt64tYaYsk/KoK42lQ= 2 | github.com/http-wasm/http-wasm-guest-tinygo v0.3.1-0.20231031134125-487a6e2eec5e/go.mod h1:zcKr7h/t5ha2ZWIMwV4iOqhfC/qno/tNPYgybVkn/MQ= 3 | -------------------------------------------------------------------------------- /tck/guest/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | 8 | httpwasm "github.com/http-wasm/http-wasm-guest-tinygo/handler" 9 | "github.com/http-wasm/http-wasm-guest-tinygo/handler/api" 10 | ) 11 | 12 | // TODO: enable_features, get_header, set_header need to be tested separately. 13 | 14 | func main() { 15 | enabledFeatures := httpwasm.Host.EnableFeatures(api.FeatureBufferRequest | api.FeatureBufferResponse | api.FeatureTrailers) 16 | h := handler{enabledFeatures: enabledFeatures} 17 | 18 | httpwasm.HandleRequestFn = h.handleRequest 19 | } 20 | 21 | type handler struct { 22 | enabledFeatures api.Features 23 | } 24 | 25 | func (h *handler) handleRequest(req api.Request, resp api.Response) (next bool, reqCtx uint32) { 26 | testID, _ := req.Headers().Get("x-httpwasm-tck-testid") 27 | if len(testID) == 0 { 28 | resp.SetStatusCode(500) 29 | resp.Body().WriteString("missing x-httpwasm-tck-testid header") 30 | return false, 0 31 | } 32 | 33 | switch testID { 34 | case "get_protocol_version": 35 | // The test runner compares this with the request value. 36 | resp.Body().WriteString(req.GetProtocolVersion()) 37 | next = true 38 | reqCtx = 0 39 | case "get_method/GET": 40 | next, reqCtx = h.testGetMethod(req, resp, "GET") 41 | case "get_method/HEAD": 42 | next, reqCtx = h.testGetMethod(req, resp, "HEAD") 43 | case "get_method/POST": 44 | next, reqCtx = h.testGetMethod(req, resp, "POST") 45 | case "get_method/PUT": 46 | next, reqCtx = h.testGetMethod(req, resp, "PUT") 47 | case "get_method/DELETE": 48 | next, reqCtx = h.testGetMethod(req, resp, "DELETE") 49 | case "get_method/CONNECT": 50 | next, reqCtx = h.testGetMethod(req, resp, "CONNECT") 51 | case "get_method/OPTIONS": 52 | next, reqCtx = h.testGetMethod(req, resp, "OPTIONS") 53 | case "get_method/TRACE": 54 | next, reqCtx = h.testGetMethod(req, resp, "TRACE") 55 | case "get_method/PATCH": 56 | next, reqCtx = h.testGetMethod(req, resp, "PATCH") 57 | case "set_method": 58 | next, reqCtx = h.testSetMethod(req, resp) 59 | case "get_uri/simple": 60 | next, reqCtx = h.testGetURI(req, resp, "/simple") 61 | case "get_uri/simple/escaping": 62 | next, reqCtx = h.testGetURI(req, resp, "/simple%26clean") 63 | case "get_uri/query": 64 | next, reqCtx = h.testGetURI(req, resp, "/animal?name=panda") 65 | case "get_uri/query/escaping": 66 | next, reqCtx = h.testGetURI(req, resp, "/disney?name=chip%26dale") 67 | case "set_uri/simple": 68 | next, reqCtx = h.testSetURI(req, resp, "/simple") 69 | case "set_uri/simple/escaping": 70 | next, reqCtx = h.testSetURI(req, resp, "/simple%26clean") 71 | case "set_uri/query": 72 | next, reqCtx = h.testSetURI(req, resp, "/animal?name=panda") 73 | case "set_uri/query/escaping": 74 | next, reqCtx = h.testSetURI(req, resp, "/disney?name=chip%26dale") 75 | case "get_header_values/request/lowercase-key": 76 | next, reqCtx = h.testGetRequestHeader(req, resp, "single-header", []string{"value"}) 77 | case "get_header_values/request/mixedcase-key": 78 | next, reqCtx = h.testGetRequestHeader(req, resp, "Single-Header", []string{"value"}) 79 | case "get_header_values/request/not-exists": 80 | next, reqCtx = h.testGetRequestHeader(req, resp, "not-header", []string{}) 81 | case "get_header_values/request/multiple-values": 82 | next, reqCtx = h.testGetRequestHeader(req, resp, "multi-header", []string{"value1", "value2"}) 83 | case "get_header_names/request": 84 | next, reqCtx = h.testGetRequestHeaderNames(req, resp, []string{"a-header", "b-header"}) 85 | case "set_header_value/request/new": 86 | next, reqCtx = h.testSetRequestHeader(req, resp, "new-header", "value") 87 | case "set_header_value/request/existing": 88 | next, reqCtx = h.testSetRequestHeader(req, resp, "existing-header", "value") 89 | case "add_header_value/request/new": 90 | next, reqCtx = h.testAddRequestHeader(req, resp, "new-header", "value") 91 | case "add_header_value/request/existing": 92 | next, reqCtx = h.testAddRequestHeader(req, resp, "existing-header", "value") 93 | case "remove_header/request/new": 94 | next, reqCtx = h.testRemoveRequestHeader(req, resp, "new-header") 95 | case "remove_header/request/existing": 96 | next, reqCtx = h.testRemoveRequestHeader(req, resp, "existing-header") 97 | case "read_body/request/empty": 98 | next, reqCtx = h.testReadBody(req, resp, "") 99 | case "read_body/request/small": 100 | next, reqCtx = h.testReadBody(req, resp, strings.Repeat("a", 5)) 101 | case "read_body/request/medium": 102 | next, reqCtx = h.testReadBody(req, resp, strings.Repeat("a", 2048)) 103 | case "read_body/request/large": 104 | next, reqCtx = h.testReadBody(req, resp, strings.Repeat("a", 4096)) 105 | case "read_body/request/xlarge": 106 | next, reqCtx = h.testReadBody(req, resp, strings.Repeat("a", 5000)) 107 | case "get_source_addr": 108 | next, reqCtx = h.testGetSourceAddr(req, resp, "127.0.0.1") 109 | default: 110 | fail(resp, "unknown x-httpwasm-test-id") 111 | } 112 | 113 | resp.Headers().Set("x-httpwasm-tck-handled", "1") 114 | 115 | return 116 | } 117 | 118 | func (h *handler) testGetMethod(req api.Request, resp api.Response, expectedMethod string) (next bool, reqCtx uint32) { 119 | if req.GetMethod() != expectedMethod { 120 | fail(resp, fmt.Sprintf("get_method: want %s, have %s", expectedMethod, req.GetMethod())) 121 | } 122 | return true, 0 123 | } 124 | 125 | func (h *handler) testSetMethod(req api.Request, _ api.Response) (next bool, reqCtx uint32) { 126 | req.SetMethod("POST") 127 | return true, 0 128 | } 129 | 130 | func (h *handler) testGetURI(req api.Request, resp api.Response, expectedURI string) (next bool, reqCtx uint32) { 131 | if req.GetURI() != expectedURI { 132 | fail(resp, fmt.Sprintf("get_uri: want %s, have %s", expectedURI, req.GetURI())) 133 | } 134 | return true, 0 135 | } 136 | 137 | func (h *handler) testSetURI(req api.Request, _ api.Response, uri string) (next bool, reqCtx uint32) { 138 | req.SetURI(uri) 139 | return true, 0 140 | } 141 | 142 | func (h *handler) testGetRequestHeader(req api.Request, resp api.Response, header string, expectedValue []string) (next bool, reqCtx uint32) { 143 | have := req.Headers().GetAll(header) 144 | if len(have) != len(expectedValue) { 145 | fail(resp, fmt.Sprintf("get_request_header: want %d values, have %d", len(expectedValue), len(have))) 146 | return 147 | } 148 | for i, v := range have { 149 | if v != expectedValue[i] { 150 | fail(resp, fmt.Sprintf("get_request_header: want %s, have %s", expectedValue[i], v)) 151 | return 152 | } 153 | } 154 | 155 | return true, 0 156 | } 157 | 158 | func (h *handler) testGetRequestHeaderNames(req api.Request, resp api.Response, expectedNames []string) (next bool, reqCtx uint32) { 159 | have := req.Headers().Names() 160 | 161 | // Don't check an exact match since it can be tricky to control automatic headers like user-agent, we're probably 162 | // fine as long as we have all the want headers. 163 | // TODO: Confirm this suspicion 164 | 165 | for _, name := range expectedNames { 166 | found := false 167 | for _, haveName := range have { 168 | if name == haveName { 169 | found = true 170 | break 171 | } 172 | } 173 | if !found { 174 | fail(resp, fmt.Sprintf("get_header_names/request: want %s, not found. have: %v", name, have)) 175 | return 176 | } 177 | } 178 | 179 | return true, 0 180 | } 181 | 182 | func (h *handler) testSetRequestHeader(req api.Request, _ api.Response, header string, value string) (next bool, reqCtx uint32) { 183 | req.Headers().Set(header, value) 184 | return true, 0 185 | } 186 | 187 | func (h *handler) testAddRequestHeader(req api.Request, _ api.Response, header string, value string) (next bool, reqCtx uint32) { 188 | req.Headers().Add(header, value) 189 | return true, 0 190 | } 191 | 192 | func (h *handler) testRemoveRequestHeader(req api.Request, _ api.Response, header string) (next bool, reqCtx uint32) { 193 | req.Headers().Remove(header) 194 | return true, 0 195 | } 196 | 197 | func (h *handler) testReadBody(req api.Request, resp api.Response, expectedBody string) (next bool, reqCtx uint32) { 198 | body := req.Body() 199 | buf := &bytes.Buffer{} 200 | sz, err := body.WriteTo(buf) 201 | if err != nil { 202 | fail(resp, fmt.Sprintf("read_body/request: error %v", err)) 203 | return 204 | } 205 | 206 | if int(sz) != len(expectedBody) { 207 | fail(resp, fmt.Sprintf("read_body/request: want %d bytes, have %d", len(expectedBody), sz)) 208 | return 209 | } 210 | 211 | if buf.String() != expectedBody { 212 | fail(resp, fmt.Sprintf("read_body/request: want %s, have %s", expectedBody, buf.String())) 213 | return 214 | } 215 | 216 | return true, 0 217 | } 218 | 219 | func (h *handler) testGetSourceAddr(req api.Request, resp api.Response, expectedAddr string) (next bool, reqCtx uint32) { 220 | addr := req.GetSourceAddr() 221 | raw := strings.Split(addr, ":") 222 | if len(raw) != 2 { 223 | fail(resp, fmt.Sprintf("get_source_addr: unknown colon count %s", req.GetSourceAddr())) 224 | return 225 | } 226 | if raw[0] != expectedAddr { 227 | fail(resp, fmt.Sprintf("get_source_addr: want %s, have %s", expectedAddr, req.GetSourceAddr())) 228 | return 229 | } 230 | if len(raw[1]) <= 0 || len(raw[1]) > 5 { 231 | fail(resp, fmt.Sprintf("get_source_addr: could not find port number '%s' from %s", raw[1], req.GetSourceAddr())) 232 | return 233 | } 234 | return true, 0 235 | } 236 | 237 | func fail(resp api.Response, msg string) { 238 | resp.SetStatusCode(500) 239 | resp.Headers().Set("x-httpwasm-tck-failed", msg) 240 | } 241 | -------------------------------------------------------------------------------- /tck/run.go: -------------------------------------------------------------------------------- 1 | package tck 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/http-wasm/http-wasm-host-go/api/handler" 12 | ) 13 | 14 | // GuestWASM is the guest wasm module used by the TCK. The host must load this 15 | // module in the server being tested by Run. 16 | // 17 | //go:embed tck.wasm 18 | var GuestWASM []byte 19 | 20 | // Run executes the TCK. The client is http.DefaultClient, or a different value 21 | // to test the HTTP/2.0 transport. The url must point to a server with the 22 | // TCK's guest wasm module loaded on top of the TCK's backend handler. 23 | // 24 | // For example, here's how to run the tests against a httptest.Server. 25 | // 26 | // server := httptest.NewServer(h) 27 | // tck.Run(t, server.Client(), server.URL) 28 | func Run(t *testing.T, client *http.Client, url string) { 29 | t.Parallel() 30 | 31 | if url == "" { 32 | t.Fatal("url is empty") 33 | } 34 | 35 | if url[len(url)-1] == '/' { 36 | url = url[:len(url)-1] 37 | } 38 | 39 | r := &testRunner{ 40 | t: t, 41 | client: client, 42 | url: url, 43 | } 44 | 45 | r.testGetProtocolVersion() 46 | r.testGetMethod() 47 | r.testSetMethod() 48 | r.testGetURI() 49 | r.testSetURI() 50 | r.testGetHeaderValuesRequest() 51 | r.testGetRequestHeaderNamesRequest() 52 | r.testSetHeaderValueRequest() 53 | r.testAddHeaderValueRequest() 54 | r.testRemoveHeaderRequest() 55 | r.testReadBodyRequest() 56 | r.testGetSourceAddr() 57 | } 58 | 59 | type testRunner struct { 60 | t *testing.T 61 | client *http.Client 62 | url string 63 | } 64 | 65 | func (r *testRunner) testGetProtocolVersion() { 66 | hostFn := handler.FuncGetProtocolVersion 67 | 68 | testID := hostFn 69 | r.t.Run(testID, func(t *testing.T) { 70 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", r.url, hostFn), nil) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | req.Header.Set("x-httpwasm-tck-testid", testID) 76 | resp, err := r.client.Do(req) 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | 81 | body := checkResponse(t, resp) 82 | 83 | want := resp.Proto 84 | have := body 85 | if want != have { 86 | t.Errorf("expected protocol version to be %s, have %s", want, have) 87 | } 88 | }) 89 | } 90 | 91 | func (r *testRunner) testGetMethod() { 92 | hostFn := handler.FuncGetMethod 93 | 94 | // TODO: CONNECT is often handled outside of middleware 95 | methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "TRACE", "PATCH"} 96 | for _, method := range methods { 97 | testID := fmt.Sprintf("%s/%s", hostFn, method) 98 | r.t.Run(testID, func(t *testing.T) { 99 | req, err := http.NewRequest(method, r.url, nil) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | req.Header.Set("x-httpwasm-tck-testid", testID) 105 | resp, err := r.client.Do(req) 106 | if err != nil { 107 | t.Error(err) 108 | } 109 | 110 | checkResponse(t, resp) 111 | }) 112 | } 113 | } 114 | 115 | func (r *testRunner) testSetMethod() { 116 | hostFn := handler.FuncSetMethod 117 | 118 | testID := hostFn 119 | r.t.Run(testID, func(t *testing.T) { 120 | req, err := http.NewRequest("GET", r.url, nil) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | req.Header.Set("x-httpwasm-tck-testid", testID) 125 | resp, err := r.client.Do(req) 126 | if err != nil { 127 | t.Error(err) 128 | } 129 | 130 | checkResponse(t, resp) 131 | 132 | if want, have := "POST", resp.Header.Get("x-httpwasm-next-method"); want != have { 133 | t.Errorf("expected method to be %s, have %s", want, have) 134 | } 135 | }) 136 | } 137 | 138 | func (r *testRunner) testGetURI() { 139 | hostFn := handler.FuncGetURI 140 | 141 | tests := []struct { 142 | testID string 143 | uri string 144 | }{ 145 | { 146 | testID: "simple", 147 | uri: "/simple", 148 | }, 149 | { 150 | testID: "simple/escaping", 151 | uri: "/simple%26clean", 152 | }, 153 | { 154 | testID: "query", 155 | uri: "/animal?name=panda", 156 | }, 157 | { 158 | testID: "query/escaping", 159 | uri: "/disney?name=chip%26dale", 160 | }, 161 | } 162 | 163 | for _, tc := range tests { 164 | tt := tc 165 | testID := fmt.Sprintf("%s/%s", hostFn, tt.testID) 166 | r.t.Run(testID, func(t *testing.T) { 167 | req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", r.url, tt.uri), nil) 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | 172 | req.Header.Set("x-httpwasm-tck-testid", testID) 173 | resp, err := r.client.Do(req) 174 | if err != nil { 175 | t.Error(err) 176 | } 177 | 178 | checkResponse(t, resp) 179 | }) 180 | } 181 | } 182 | 183 | func (r *testRunner) testSetURI() { 184 | hostFn := handler.FuncSetURI 185 | 186 | tests := []struct { 187 | testID string 188 | uri string 189 | }{ 190 | { 191 | testID: "simple", 192 | uri: "/simple", 193 | }, 194 | { 195 | testID: "simple/escaping", 196 | uri: "/simple%26clean", 197 | }, 198 | { 199 | testID: "query", 200 | uri: "/animal?name=panda", 201 | }, 202 | { 203 | testID: "query/escaping", 204 | uri: "/disney?name=chip%26dale", 205 | }, 206 | } 207 | 208 | for _, tc := range tests { 209 | tt := tc 210 | testID := fmt.Sprintf("%s/%s", hostFn, tt.testID) 211 | r.t.Run(testID, func(t *testing.T) { 212 | req, err := http.NewRequest("GET", r.url, nil) 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | 217 | req.Header.Set("x-httpwasm-tck-testid", testID) 218 | resp, err := r.client.Do(req) 219 | if err != nil { 220 | t.Error(err) 221 | } 222 | 223 | checkResponse(t, resp) 224 | 225 | if want, have := tt.uri, resp.Header.Get("x-httpwasm-next-uri"); want != have { 226 | t.Errorf("expected uri to be %s, have %s", want, have) 227 | } 228 | }) 229 | } 230 | } 231 | 232 | func (r *testRunner) testGetHeaderValuesRequest() { 233 | hostFn := handler.FuncGetHeaderValues 234 | 235 | tests := []struct { 236 | testID string 237 | key string 238 | value []string 239 | }{ 240 | { 241 | testID: "lowercase-key", 242 | key: "single-header", 243 | value: []string{"value"}, 244 | }, 245 | { 246 | testID: "mixedcase-key", 247 | key: "Single-Header", 248 | value: []string{"value"}, 249 | }, 250 | { 251 | testID: "not-exists", 252 | key: "not-header", 253 | value: []string{}, 254 | }, 255 | { 256 | testID: "multiple-values", 257 | key: "multi-header", 258 | value: []string{"value1", "value2"}, 259 | }, 260 | } 261 | 262 | for _, tc := range tests { 263 | tt := tc 264 | testID := fmt.Sprintf("%s/request/%s", hostFn, tt.testID) 265 | r.t.Run(testID, func(t *testing.T) { 266 | req, err := http.NewRequest("GET", r.url, nil) 267 | if err != nil { 268 | t.Fatal(err) 269 | } 270 | 271 | for _, v := range tt.value { 272 | req.Header.Add(tt.key, v) 273 | } 274 | 275 | req.Header.Set("x-httpwasm-tck-testid", testID) 276 | resp, err := r.client.Do(req) 277 | if err != nil { 278 | t.Error(err) 279 | } 280 | 281 | checkResponse(t, resp) 282 | }) 283 | } 284 | } 285 | 286 | func (r *testRunner) testGetRequestHeaderNamesRequest() { 287 | hostFn := handler.FuncGetHeaderNames 288 | 289 | testID := hostFn + "/request" 290 | r.t.Run(testID, func(t *testing.T) { 291 | req, err := http.NewRequest("GET", r.url, nil) 292 | if err != nil { 293 | t.Fatal(err) 294 | } 295 | 296 | req.Header.Add("a-header", "value1") 297 | req.Header.Add("b-header", "2") 298 | 299 | req.Header.Set("x-httpwasm-tck-testid", testID) 300 | resp, err := r.client.Do(req) 301 | if err != nil { 302 | t.Error(err) 303 | } 304 | 305 | checkResponse(t, resp) 306 | }) 307 | } 308 | 309 | func (r *testRunner) testReadBodyRequest() { 310 | hostFn := handler.FuncReadBody 311 | 312 | tests := []struct { 313 | testID string 314 | bodySize int 315 | }{ 316 | { 317 | testID: "empty", 318 | bodySize: 0, 319 | }, 320 | { 321 | testID: "small", 322 | bodySize: 5, 323 | }, 324 | { 325 | testID: "medium", 326 | bodySize: 2048, 327 | }, 328 | { 329 | testID: "large", 330 | bodySize: 4096, 331 | }, 332 | { 333 | testID: "xlarge", 334 | bodySize: 5000, 335 | }, 336 | } 337 | 338 | for _, tc := range tests { 339 | tt := tc 340 | testID := fmt.Sprintf("%s/request/%s", hostFn, tt.testID) 341 | r.t.Run(testID, func(t *testing.T) { 342 | payload := strings.Repeat("a", tt.bodySize) 343 | req, err := http.NewRequest("POST", r.url, strings.NewReader(payload)) 344 | if err != nil { 345 | t.Fatal(err) 346 | } 347 | 348 | req.Header.Set("x-httpwasm-tck-testid", testID) 349 | resp, err := r.client.Do(req) 350 | if err != nil { 351 | t.Error(err) 352 | } 353 | 354 | checkResponse(t, resp) 355 | }) 356 | } 357 | } 358 | 359 | func (r *testRunner) testSetHeaderValueRequest() { 360 | hostFn := handler.FuncSetHeaderValue 361 | 362 | tests := []struct { 363 | name string 364 | header string 365 | }{ 366 | { 367 | name: "new", 368 | header: "new-header", 369 | }, 370 | { 371 | name: "existing", 372 | header: "existing-header", 373 | }, 374 | } 375 | for _, tc := range tests { 376 | tt := tc 377 | testID := fmt.Sprintf("%s/request/%s", hostFn, tt.name) 378 | r.t.Run(testID, func(t *testing.T) { 379 | req, err := http.NewRequest("GET", r.url, nil) 380 | if err != nil { 381 | t.Fatal(err) 382 | } 383 | 384 | req.Header.Set("existing-header", "bear") 385 | 386 | req.Header.Set("x-httpwasm-tck-testid", testID) 387 | resp, err := r.client.Do(req) 388 | if err != nil { 389 | t.Error(err) 390 | } 391 | 392 | checkResponse(t, resp) 393 | 394 | if want, have := "value", resp.Header.Get(fmt.Sprintf("x-httpwasm-next-header-%s-0", tt.header)); want != have { 395 | t.Errorf("expected header to be %s, have %s", want, have) 396 | } 397 | }) 398 | } 399 | } 400 | 401 | func (r *testRunner) testAddHeaderValueRequest() { 402 | hostFn := handler.FuncAddHeaderValue 403 | 404 | tests := []struct { 405 | name string 406 | header string 407 | values []string 408 | }{ 409 | { 410 | name: "new", 411 | header: "new-header", 412 | values: []string{"value"}, 413 | }, 414 | { 415 | name: "existing", 416 | header: "existing-header", 417 | values: []string{"bear", "value"}, 418 | }, 419 | } 420 | for _, tc := range tests { 421 | tt := tc 422 | testID := fmt.Sprintf("%s/request/%s", hostFn, tt.name) 423 | r.t.Run(testID, func(t *testing.T) { 424 | req, err := http.NewRequest("GET", r.url, nil) 425 | if err != nil { 426 | t.Fatal(err) 427 | } 428 | 429 | req.Header.Set("existing-header", "bear") 430 | 431 | req.Header.Set("x-httpwasm-tck-testid", testID) 432 | resp, err := r.client.Do(req) 433 | if err != nil { 434 | t.Error(err) 435 | } 436 | 437 | checkResponse(t, resp) 438 | 439 | for i, v := range tt.values { 440 | if want, have := v, resp.Header.Get(fmt.Sprintf("x-httpwasm-next-header-%s-%d", tt.header, i)); want != have { 441 | t.Errorf("expected header to be %s, have %s", want, have) 442 | } 443 | } 444 | }) 445 | } 446 | } 447 | 448 | func (r *testRunner) testRemoveHeaderRequest() { 449 | hostFn := handler.FuncRemoveHeader 450 | 451 | tests := []struct { 452 | name string 453 | header string 454 | removed bool 455 | }{ 456 | { 457 | name: "new", 458 | header: "new-header", 459 | removed: false, 460 | }, 461 | { 462 | name: "existing", 463 | header: "existing-header", 464 | removed: true, 465 | }, 466 | } 467 | for _, tc := range tests { 468 | tt := tc 469 | testID := fmt.Sprintf("%s/request/%s", hostFn, tt.name) 470 | r.t.Run(testID, func(t *testing.T) { 471 | req, err := http.NewRequest("GET", r.url, nil) 472 | if err != nil { 473 | t.Fatal(err) 474 | } 475 | 476 | req.Header.Set("existing-header", "bear") 477 | 478 | req.Header.Set("x-httpwasm-tck-testid", testID) 479 | resp, err := r.client.Do(req) 480 | if err != nil { 481 | t.Error(err) 482 | } 483 | 484 | checkResponse(t, resp) 485 | 486 | want := "bear" 487 | if tt.removed { 488 | want = "" 489 | } 490 | if have := resp.Header.Get("x-httpwasm-next-header-existing-header-0"); want != have { 491 | t.Errorf("expected header to be %s, have %s", want, have) 492 | } 493 | }) 494 | } 495 | } 496 | 497 | func (r *testRunner) testGetSourceAddr() { 498 | hostFn := handler.FuncGetSourceAddr 499 | 500 | r.t.Run(hostFn, func(t *testing.T) { 501 | req, err := http.NewRequest("GET", r.url, nil) 502 | if err != nil { 503 | t.Fatal(err) 504 | } 505 | 506 | req.Header.Set("x-httpwasm-tck-testid", hostFn) 507 | resp, err := r.client.Do(req) 508 | if err != nil { 509 | t.Error(err) 510 | } 511 | checkResponse(t, resp) 512 | }) 513 | } 514 | 515 | func checkResponse(t *testing.T, resp *http.Response) string { 516 | t.Helper() 517 | 518 | if resp.Header.Get("x-httpwasm-tck-handled") != "1" { 519 | t.Error("x-httpwasm-tck-handled header is missing") 520 | } 521 | 522 | if resp.StatusCode == http.StatusInternalServerError { 523 | msg := resp.Header.Get("x-httpwasm-tck-failed") 524 | if msg == "" { 525 | t.Error("error status without test failure message") 526 | } 527 | t.Errorf("assertion failed: %s", msg) 528 | } 529 | 530 | body, err := io.ReadAll(resp.Body) 531 | if err != nil { 532 | t.Error("error reading response body") 533 | } 534 | return string(body) 535 | } 536 | -------------------------------------------------------------------------------- /tck/tck.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-wasm/http-wasm-host-go/d9e07495db6c4192ce0995de036e2ad450616883/tck/tck.wasm -------------------------------------------------------------------------------- /testing/handlertest/testhandler.go: -------------------------------------------------------------------------------- 1 | // Package handlertest implements support for testing implementations 2 | // of HTTP handlers. 3 | // 4 | // This is inspired by fstest.TestFS, but implemented differently, notably 5 | // using a testing.T parameter for better reporting. 6 | package handlertest 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | "reflect" 12 | "sort" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/http-wasm/http-wasm-host-go/api/handler" 17 | ) 18 | 19 | // HostTest tests a handler.Host by testing default property values and 20 | // ability to change them. 21 | // 22 | // To use this, pass your host and the context which allows access to the 23 | // request and response objects. This should be configured for "HTTP/1.1". 24 | // 25 | // Here's an example of a net/http host which supports no features: 26 | // 27 | // newCtx := func(features handler.Features) (context.Context, handler.Features) { 28 | // if features != 0 { 29 | // return testCtx, 0 // unsupported 30 | // } 31 | // 32 | // r, _ := http.NewRequest("GET", "", nil) 33 | // w := &httptest.ResponseRecorder{HeaderMap: map[string][]string{}} 34 | // return context.WithValue(testCtx, requestStateKey{}, &requestState{r: r, w: w}), features 35 | // } 36 | // 37 | // // Run all the tests 38 | // handlertest.HostTest(t, host{}, newCtx) 39 | func HostTest(t *testing.T, h handler.Host, newCtx func(handler.Features) (context.Context, handler.Features)) error { 40 | ht := hostTester{t: t, h: h, newCtx: newCtx} 41 | 42 | ht.testMethod() 43 | ht.testURI() 44 | ht.testProtocolVersion() 45 | ht.testRequestHeaders() 46 | ht.testRequestBody() 47 | ht.testRequestTrailers() 48 | ht.testStatusCode() 49 | ht.testResponseHeaders() 50 | ht.testResponseBody() 51 | ht.testResponseTrailers() 52 | ht.testSourceAddr() 53 | 54 | if len(ht.errText) == 0 { 55 | return nil 56 | } 57 | return errors.New("TestHost found errors:\n" + string(ht.errText)) 58 | } 59 | 60 | // A hostTester holds state for running the test. 61 | type hostTester struct { 62 | t *testing.T 63 | h handler.Host 64 | newCtx func(handler.Features) (context.Context, handler.Features) 65 | errText []byte 66 | } 67 | 68 | func (h *hostTester) testSourceAddr() { 69 | ctx, _ := h.newCtx(0) // no features required 70 | 71 | h.t.Run("GetSourceAddr", func(t *testing.T) { 72 | addr := h.h.GetSourceAddr(ctx) 73 | want := "1.2.3.4:12345" 74 | if addr != want { 75 | t.Errorf("unexpected default source addr, want: %v, have: %v", want, addr) 76 | } 77 | }) 78 | } 79 | 80 | func (h *hostTester) testMethod() { 81 | ctx, _ := h.newCtx(0) // no features required 82 | 83 | h.t.Run("GetMethod default", func(t *testing.T) { 84 | // Check default 85 | if want, have := "GET", h.h.GetMethod(ctx); want != have { 86 | t.Errorf("unexpected default method, want: %v, have: %v", want, have) 87 | } 88 | }) 89 | 90 | h.t.Run("SetMethod", func(t *testing.T) { 91 | for _, want := range []string{"POST", "OPTIONS"} { 92 | h.h.SetMethod(ctx, want) 93 | 94 | if have := h.h.GetMethod(ctx); want != have { 95 | t.Errorf("unexpected method, set: %v, have: %v", want, have) 96 | } 97 | } 98 | }) 99 | } 100 | 101 | func (h *hostTester) testURI() { 102 | ctx, _ := h.newCtx(0) // no features required 103 | 104 | h.t.Run("GetURI default", func(t *testing.T) { 105 | if want, have := "/", h.h.GetURI(ctx); want != have { 106 | t.Errorf("unexpected default URI, want: %v, have: %v", want, have) 107 | } 108 | }) 109 | 110 | tests := []struct { 111 | name string 112 | set string 113 | want string 114 | }{ 115 | { 116 | want: "/", 117 | }, 118 | { 119 | set: "/a b", 120 | want: "/a%20b", 121 | }, 122 | { 123 | set: "/a b?q=go+language", 124 | want: "/a%20b?q=go+language", 125 | }, 126 | { 127 | set: "/a b?q=go language", 128 | want: "/a%20b?q=go language", 129 | }, 130 | { 131 | set: "//foo", 132 | want: "//foo", 133 | }, 134 | { 135 | set: "/foo?", 136 | want: "/foo?", 137 | }, 138 | } 139 | 140 | h.t.Run("SetURI", func(t *testing.T) { 141 | for _, tc := range tests { 142 | 143 | h.h.SetURI(ctx, tc.set) 144 | 145 | if have := h.h.GetURI(ctx); tc.want != have { 146 | t.Errorf("unexpected URI, set: %v, want: %v, have: %v", tc.set, tc.want, have) 147 | } 148 | } 149 | }) 150 | } 151 | 152 | func (h *hostTester) testProtocolVersion() { 153 | ctx, _ := h.newCtx(0) // no features required 154 | 155 | h.t.Run("GetProtocolVersion default", func(t *testing.T) { 156 | if want, have := "HTTP/1.1", h.h.GetProtocolVersion(ctx); want != have { 157 | t.Errorf("unexpected protocol version, want: %v, have: %v", want, have) 158 | } 159 | }) 160 | } 161 | 162 | func (h *hostTester) testRequestHeaders() { 163 | h.testRequestHeaderNames() 164 | h.testGetRequestHeaderValues() 165 | h.testSetRequestHeaderValue() 166 | h.testAddRequestHeaderValue() 167 | h.testRemoveRequestHeaderValue() 168 | } 169 | 170 | func (h *hostTester) addTestRequestHeaders(ctx context.Context) { 171 | for k, vs := range testRequestHeaders { 172 | h.h.SetRequestHeaderValue(ctx, k, vs[0]) 173 | for _, v := range vs[1:] { 174 | h.h.AddRequestHeaderValue(ctx, k, v) 175 | } 176 | } 177 | } 178 | 179 | func (h *hostTester) testRequestHeaderNames() { 180 | ctx, _ := h.newCtx(0) // no features required 181 | 182 | h.t.Run("GetRequestHeaderNames default", func(t *testing.T) { 183 | if h.h.GetRequestHeaderNames(ctx) != nil { 184 | t.Errorf("unexpected default request header names, want: nil") 185 | } 186 | }) 187 | 188 | h.t.Run("GetRequestHeaderNames", func(t *testing.T) { 189 | h.addTestRequestHeaders(ctx) 190 | 191 | var want []string 192 | for k := range testRequestHeaders { 193 | want = append(want, k) 194 | } 195 | sort.Strings(want) 196 | 197 | have := h.h.GetRequestHeaderNames(ctx) 198 | sort.Strings(have) 199 | if !reflect.DeepEqual(want, have) { 200 | t.Errorf("unexpected header names, want: %v, have: %v", want, have) 201 | } 202 | }) 203 | } 204 | 205 | func (h *hostTester) testGetRequestHeaderValues() { 206 | ctx, _ := h.newCtx(0) // no features required 207 | h.addTestRequestHeaders(ctx) 208 | 209 | tests := []struct { 210 | name string 211 | headerName string 212 | want []string 213 | }{ 214 | { 215 | name: "single value", 216 | headerName: "Content-Type", 217 | want: []string{"text/plain"}, 218 | }, 219 | { 220 | name: "multi-field with comma value", 221 | headerName: "X-Forwarded-For", 222 | want: []string{"client, proxy1", "proxy2"}, 223 | }, 224 | { 225 | name: "empty value", 226 | headerName: "Empty", 227 | want: []string{""}, 228 | }, 229 | { 230 | name: "no value", 231 | headerName: "Not Found", 232 | }, 233 | } 234 | 235 | h.t.Run("GetRequestHeaderValues", func(t *testing.T) { 236 | for _, tc := range tests { 237 | values := h.h.GetRequestHeaderValues(ctx, tc.headerName) 238 | 239 | if want, have := tc.want, values; !reflect.DeepEqual(want, have) { 240 | t.Errorf("%s: unexpected header values, want: %v, have: %v", tc.name, want, have) 241 | } 242 | } 243 | }) 244 | } 245 | 246 | func (h *hostTester) testSetRequestHeaderValue() { 247 | ctx, _ := h.newCtx(0) // no features required 248 | h.addTestRequestHeaders(ctx) 249 | 250 | tests := []struct { 251 | name string 252 | headerName string 253 | want string 254 | }{ 255 | { 256 | name: "non-existing", 257 | headerName: "custom", 258 | want: "1", 259 | }, 260 | { 261 | name: "existing", 262 | headerName: "Content-Type", 263 | want: "application/json", 264 | }, 265 | { 266 | name: "existing lowercase", 267 | headerName: "content-type", 268 | want: "application/json", 269 | }, 270 | { 271 | name: "set to empty", 272 | headerName: "Custom", 273 | }, 274 | { 275 | name: "multi-field", 276 | headerName: "X-Forwarded-For", 277 | want: "proxy2", 278 | }, 279 | { 280 | name: "set multi-field to empty", 281 | headerName: "X-Forwarded-For", 282 | want: "", 283 | }, 284 | } 285 | 286 | h.t.Run("SetRequestHeaderValue", func(t *testing.T) { 287 | for _, tc := range tests { 288 | h.h.SetRequestHeaderValue(ctx, tc.headerName, tc.want) 289 | 290 | if want, have := tc.want, strings.Join(h.h.GetRequestHeaderValues(ctx, tc.headerName), "|"); want != have { 291 | t.Errorf("%s: unexpected header, want: %v, have: %v", tc.name, want, have) 292 | } 293 | } 294 | }) 295 | } 296 | 297 | func (h *hostTester) testAddRequestHeaderValue() { 298 | tests := []struct { 299 | name string 300 | headerName string 301 | value string 302 | want []string 303 | }{ 304 | { 305 | name: "non-existing", 306 | headerName: "new", 307 | value: "1", 308 | want: []string{"1"}, 309 | }, 310 | { 311 | name: "empty", 312 | headerName: "new", 313 | want: []string{""}, 314 | }, 315 | { 316 | name: "existing", 317 | headerName: "X-Forwarded-For", 318 | value: "proxy3", 319 | want: []string{"client, proxy1", "proxy2", "proxy3"}, 320 | }, 321 | { 322 | name: "lowercase", 323 | headerName: "x-forwarded-for", 324 | value: "proxy3", 325 | want: []string{"client, proxy1", "proxy2", "proxy3"}, 326 | }, 327 | { 328 | name: "existing empty", 329 | headerName: "X-Forwarded-For", 330 | want: []string{"client, proxy1", "proxy2", ""}, 331 | }, 332 | } 333 | 334 | h.t.Run("AddRequestHeaderValue", func(t *testing.T) { 335 | for _, tc := range tests { 336 | ctx, _ := h.newCtx(0) // no features required 337 | h.addTestRequestHeaders(ctx) 338 | 339 | h.h.AddRequestHeaderValue(ctx, tc.headerName, tc.value) 340 | 341 | if want, have := tc.want, h.h.GetRequestHeaderValues(ctx, tc.headerName); !reflect.DeepEqual(want, have) { 342 | t.Errorf("%s: unexpected header, want: %v, have: %v", tc.name, want, have) 343 | } 344 | } 345 | }) 346 | } 347 | 348 | func (h *hostTester) testRemoveRequestHeaderValue() { 349 | ctx, _ := h.newCtx(0) // no features required 350 | h.addTestRequestHeaders(ctx) 351 | 352 | tests := []struct { 353 | name string 354 | headerName string 355 | }{ 356 | { 357 | name: "doesn't exist", 358 | headerName: "custom", 359 | }, 360 | { 361 | name: "empty", 362 | headerName: "Empty", 363 | }, 364 | { 365 | name: "exists", 366 | headerName: "Custom", 367 | }, 368 | { 369 | name: "lowercase", 370 | headerName: "custom", 371 | }, 372 | { 373 | name: "multi-field", 374 | headerName: "X-Forwarded-For", 375 | }, 376 | } 377 | 378 | h.t.Run("RemoveRequestHeader", func(t *testing.T) { 379 | for _, tc := range tests { 380 | h.h.RemoveRequestHeader(ctx, tc.headerName) 381 | 382 | if have := h.h.GetRequestHeaderValues(ctx, tc.headerName); len(have) > 0 { 383 | t.Errorf("%s: unexpected headers: %v", tc.name, have) 384 | } 385 | } 386 | }) 387 | } 388 | 389 | func (h *hostTester) testRequestBody() { 390 | // All body tests require read-back 391 | ctx, enabled := h.newCtx(handler.FeatureBufferRequest) 392 | if !enabled.IsEnabled(handler.FeatureBufferRequest) { 393 | return 394 | } 395 | 396 | h.t.Run("RequestBodyReader default", func(t *testing.T) { 397 | if h.h.RequestBodyReader(ctx) == nil { 398 | t.Errorf("unexpected default body reader, want: != nil") 399 | } 400 | }) 401 | } 402 | 403 | func (h *hostTester) testRequestTrailers() { 404 | ctx, enabled := h.newCtx(handler.FeatureTrailers) 405 | if !enabled.IsEnabled(handler.FeatureTrailers) { 406 | return 407 | } 408 | 409 | h.t.Run("GetRequestTrailerNames default", func(t *testing.T) { 410 | if h.h.GetRequestTrailerNames(ctx) != nil { 411 | t.Errorf("unexpected default trailer names, want: nil") 412 | } 413 | }) 414 | } 415 | 416 | func (h *hostTester) testStatusCode() { 417 | // We can't test setting a response property without reading it back. 418 | // Read-back of any response property requires buffering. 419 | ctx, enabled := h.newCtx(handler.FeatureBufferResponse) 420 | if !enabled.IsEnabled(handler.FeatureBufferResponse) { 421 | return 422 | } 423 | 424 | h.t.Run("GetStatusCode default", func(t *testing.T) { 425 | if want, have := uint32(200), h.h.GetStatusCode(ctx); want != have { 426 | t.Errorf("unexpected default status code, want: %v, have: %v", want, have) 427 | } 428 | }) 429 | } 430 | 431 | func (h *hostTester) testResponseHeaders() { 432 | h.testResponseHeaderNames() 433 | h.testGetResponseHeaderValues() 434 | h.testSetResponseHeaderValue() 435 | h.testAddResponseHeaderValue() 436 | h.testRemoveResponseHeaderValue() 437 | } 438 | 439 | func (h *hostTester) addTestResponseHeaders(ctx context.Context) { 440 | for k, vs := range testResponseHeaders { 441 | h.h.SetResponseHeaderValue(ctx, k, vs[0]) 442 | for _, v := range vs[1:] { 443 | h.h.AddResponseHeaderValue(ctx, k, v) 444 | } 445 | } 446 | } 447 | 448 | func (h *hostTester) testResponseHeaderNames() { 449 | ctx, _ := h.newCtx(0) // no features required 450 | 451 | h.t.Run("GetResponseHeaderNames default", func(t *testing.T) { 452 | if h.h.GetResponseHeaderNames(ctx) != nil { 453 | t.Errorf("unexpected default response header names, want: nil") 454 | } 455 | }) 456 | 457 | h.t.Run("GetResponseHeaderNames", func(t *testing.T) { 458 | h.addTestResponseHeaders(ctx) 459 | 460 | var want []string 461 | for k := range testResponseHeaders { 462 | want = append(want, k) 463 | } 464 | sort.Strings(want) 465 | 466 | have := h.h.GetResponseHeaderNames(ctx) 467 | sort.Strings(have) 468 | if !reflect.DeepEqual(want, have) { 469 | t.Errorf("unexpected header names, want: %v, have: %v", want, have) 470 | } 471 | }) 472 | } 473 | 474 | func (h *hostTester) testGetResponseHeaderValues() { 475 | ctx, _ := h.newCtx(0) // no features required 476 | h.addTestResponseHeaders(ctx) 477 | 478 | tests := []struct { 479 | name string 480 | headerName string 481 | want []string 482 | }{ 483 | { 484 | name: "single value", 485 | headerName: "Content-Type", 486 | want: []string{"text/plain"}, 487 | }, 488 | { 489 | name: "multi-field with comma value", 490 | headerName: "Set-Cookie", 491 | want: []string{"a=b, c=d", "e=f"}, 492 | }, 493 | { 494 | name: "empty value", 495 | headerName: "Empty", 496 | want: []string{""}, 497 | }, 498 | { 499 | name: "no value", 500 | headerName: "Not Found", 501 | }, 502 | } 503 | 504 | h.t.Run("GetResponseHeaderValues", func(t *testing.T) { 505 | for _, tc := range tests { 506 | values := h.h.GetResponseHeaderValues(ctx, tc.headerName) 507 | 508 | if want, have := tc.want, values; !reflect.DeepEqual(want, have) { 509 | t.Errorf("%s: unexpected header values, want: %v, have: %v", tc.name, want, have) 510 | } 511 | } 512 | }) 513 | } 514 | 515 | func (h *hostTester) testSetResponseHeaderValue() { 516 | ctx, _ := h.newCtx(0) // no features required 517 | h.addTestResponseHeaders(ctx) 518 | 519 | tests := []struct { 520 | name string 521 | headerName string 522 | want string 523 | }{ 524 | { 525 | name: "non-existing", 526 | headerName: "custom", 527 | want: "1", 528 | }, 529 | { 530 | name: "existing", 531 | headerName: "Content-Type", 532 | want: "application/json", 533 | }, 534 | { 535 | name: "existing lowercase", 536 | headerName: "content-type", 537 | want: "application/json", 538 | }, 539 | { 540 | name: "set to empty", 541 | headerName: "Custom", 542 | }, 543 | { 544 | name: "multi-field", 545 | headerName: "Set-Cookie", 546 | want: "proxy2", 547 | }, 548 | { 549 | name: "set multi-field to empty", 550 | headerName: "Set-Cookie", 551 | want: "", 552 | }, 553 | } 554 | 555 | h.t.Run("SetResponseHeaderValue", func(t *testing.T) { 556 | for _, tc := range tests { 557 | h.h.SetResponseHeaderValue(ctx, tc.headerName, tc.want) 558 | 559 | if want, have := tc.want, strings.Join(h.h.GetResponseHeaderValues(ctx, tc.headerName), "|"); want != have { 560 | t.Errorf("%s: unexpected header, want: %v, have: %v", tc.name, want, have) 561 | } 562 | } 563 | }) 564 | } 565 | 566 | func (h *hostTester) testAddResponseHeaderValue() { 567 | tests := []struct { 568 | name string 569 | headerName string 570 | value string 571 | want []string 572 | }{ 573 | { 574 | name: "non-existing", 575 | headerName: "new", 576 | value: "1", 577 | want: []string{"1"}, 578 | }, 579 | { 580 | name: "empty", 581 | headerName: "new", 582 | want: []string{""}, 583 | }, 584 | { 585 | name: "existing", 586 | headerName: "Set-Cookie", 587 | value: "g=h", 588 | want: []string{"a=b, c=d", "e=f", "g=h"}, 589 | }, 590 | { 591 | name: "lowercase", 592 | headerName: "set-Cookie", 593 | value: "g=h", 594 | want: []string{"a=b, c=d", "e=f", "g=h"}, 595 | }, 596 | { 597 | name: "existing empty", 598 | headerName: "Set-Cookie", 599 | value: "", 600 | want: []string{"a=b, c=d", "e=f", ""}, 601 | }, 602 | } 603 | 604 | h.t.Run("AddResponseHeaderValue", func(t *testing.T) { 605 | for _, tc := range tests { 606 | ctx, _ := h.newCtx(0) // no features required 607 | h.addTestResponseHeaders(ctx) 608 | 609 | h.h.AddResponseHeaderValue(ctx, tc.headerName, tc.value) 610 | 611 | if want, have := tc.want, h.h.GetResponseHeaderValues(ctx, tc.headerName); !reflect.DeepEqual(want, have) { 612 | t.Errorf("%s: unexpected header, want: %v, have: %v", tc.name, want, have) 613 | } 614 | } 615 | }) 616 | } 617 | 618 | func (h *hostTester) testRemoveResponseHeaderValue() { 619 | ctx, _ := h.newCtx(0) // no features required 620 | h.addTestResponseHeaders(ctx) 621 | 622 | tests := []struct { 623 | name string 624 | headerName string 625 | }{ 626 | { 627 | name: "doesn't exist", 628 | headerName: "new", 629 | }, 630 | { 631 | name: "empty", 632 | headerName: "Empty", 633 | }, 634 | { 635 | name: "exists", 636 | headerName: "Custom", 637 | }, 638 | { 639 | name: "lowercase", 640 | headerName: "custom", 641 | }, 642 | { 643 | name: "multi-field", 644 | headerName: "Set-Cookie", 645 | }, 646 | } 647 | 648 | h.t.Run("RemoveResponseHeader", func(t *testing.T) { 649 | for _, tc := range tests { 650 | h.h.RemoveResponseHeader(ctx, tc.headerName) 651 | 652 | if have := h.h.GetResponseHeaderValues(ctx, tc.headerName); len(have) > 0 { 653 | t.Errorf("%s: unexpected headers: %v", tc.name, have) 654 | } 655 | } 656 | }) 657 | } 658 | 659 | func (h *hostTester) testResponseBody() { 660 | // We can't test setting a response property without reading it back. 661 | // Read-back of any response property requires buffering. 662 | ctx, enabled := h.newCtx(handler.FeatureBufferResponse) 663 | if !enabled.IsEnabled(handler.FeatureBufferResponse) { 664 | return 665 | } 666 | 667 | h.t.Run("ResponseBodyReader default", func(t *testing.T) { 668 | if h.h.ResponseBodyReader(ctx) == nil { 669 | t.Errorf("unexpected default body reader, want: != nil") 670 | } 671 | }) 672 | } 673 | 674 | func (h *hostTester) testResponseTrailers() { 675 | // We can't test setting a response property without reading it back. 676 | // Read-back of any response property requires buffering, and trailers 677 | // requires an additional feature 678 | requiredFeatures := handler.FeatureTrailers | handler.FeatureBufferResponse 679 | ctx, enabled := h.newCtx(requiredFeatures) 680 | if !enabled.IsEnabled(requiredFeatures) { 681 | return 682 | } 683 | 684 | h.t.Run("GetResponseTrailerNames default", func(t *testing.T) { 685 | if h.h.GetResponseTrailerNames(ctx) != nil { 686 | t.Errorf("unexpected default trailer names, want: nil") 687 | } 688 | }) 689 | } 690 | 691 | // Note: senders are supposed to concatenate multiple fields with the same 692 | // name on comma, except the response header Set-Cookie. That said, a lot 693 | // of middleware don't know about this and may repeat other headers anyway. 694 | // See https://www.rfc-editoreqHeaders.org/rfc/rfc9110#section-5.2 695 | 696 | var ( 697 | testRequestHeaders = map[string][]string{ 698 | "Content-Type": {"text/plain"}, 699 | "Custom": {"1"}, 700 | "X-Forwarded-For": {"client, proxy1", "proxy2"}, 701 | "Empty": {""}, 702 | } 703 | testResponseHeaders = map[string][]string{ 704 | "Content-Type": {"text/plain"}, 705 | "Custom": {"1"}, 706 | "Set-Cookie": {"a=b, c=d", "e=f"}, 707 | "Empty": {""}, 708 | } 709 | ) 710 | --------------------------------------------------------------------------------