Repository: grpc/grpc-web Branch: master Commit: 1bcab087f1d3 Files: 174 Total size: 467.7 KB Directory structure: gitextract_uo_g1mbd/ ├── .bazelci/ │ └── presubmit.yml ├── .bazelignore ├── .bazelrc ├── .bazelrc.windows ├── .dockerignore ├── .github/ │ └── workflows/ │ ├── make-plugin-linux.yml │ ├── make-plugin-mac-os.yml │ ├── make-plugin-windows.yml │ └── release-source-archive.yml ├── .gitignore ├── .gitmodules ├── AUTHORS ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── GOVERNANCE.md ├── LICENSE ├── MAINTAINERS.md ├── MODULE.bazel ├── Makefile ├── PATENTS ├── README.md ├── SECURITY.md ├── doc/ │ ├── browser-features.md │ ├── in-process-proxy.md │ ├── interop-test-descriptions.md │ ├── roadmap.md │ └── streaming-roadmap.md ├── docker-compose.yml ├── etc/ │ ├── localhost.crt │ └── localhost.key ├── javascript/ │ └── net/ │ └── grpc/ │ └── web/ │ ├── abstractclientbase.js │ ├── calloptions.js │ ├── clientoptions.js │ ├── clientreadablestream.js │ ├── clientunarycallimpl.js │ ├── generator/ │ │ ├── BUILD.bazel │ │ ├── Makefile │ │ └── grpc_generator.cc │ ├── generictransportinterface.js │ ├── grpcwebclientbase.js │ ├── grpcwebclientbase_test.js │ ├── grpcwebclientreadablestream.js │ ├── grpcwebstreamparser.js │ ├── grpcwebstreamparser_test.js │ ├── interceptor.js │ ├── metadata.js │ ├── methoddescriptor.js │ ├── methoddescriptorinterface.js │ ├── methodtype.js │ ├── request.js │ ├── requestinternal.js │ ├── rpcerror.js │ ├── status.js │ ├── statuscode.js │ ├── statuscode_test.js │ ├── unaryresponse.js │ └── unaryresponseinternal.js ├── kokoro/ │ ├── interop.cfg │ ├── master.cfg │ └── presubmit.cfg ├── net/ │ └── grpc/ │ └── gateway/ │ ├── docker/ │ │ ├── binary_client/ │ │ │ └── Dockerfile │ │ ├── closure_client/ │ │ │ └── Dockerfile │ │ ├── commonjs_client/ │ │ │ └── Dockerfile │ │ ├── echo_server/ │ │ │ └── Dockerfile │ │ ├── envoy/ │ │ │ └── Dockerfile │ │ ├── grpcwebproxy/ │ │ │ └── Dockerfile │ │ ├── interop_client/ │ │ │ └── Dockerfile │ │ ├── node_interop_server/ │ │ │ └── Dockerfile │ │ ├── node_server/ │ │ │ └── Dockerfile │ │ ├── prereqs/ │ │ │ └── Dockerfile │ │ └── ts_client/ │ │ └── Dockerfile │ └── examples/ │ ├── echo/ │ │ ├── .gitignore │ │ ├── BUILD.bazel │ │ ├── Makefile │ │ ├── README.md │ │ ├── commonjs-example/ │ │ │ ├── .gitignore │ │ │ ├── client.js │ │ │ ├── echotest.html │ │ │ ├── package.json │ │ │ └── webpack.config.js │ │ ├── echo.proto │ │ ├── echo_chat.js │ │ ├── echo_server.cc │ │ ├── echo_service_impl.cc │ │ ├── echo_service_impl.h │ │ ├── echoapp.js │ │ ├── echotest.html │ │ ├── envoy.yaml │ │ ├── node-server/ │ │ │ ├── .gitignore │ │ │ ├── package.json │ │ │ └── server.js │ │ ├── package.json │ │ ├── ts-example/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── client.ts │ │ │ ├── echotest.html │ │ │ ├── package.json │ │ │ ├── tsconfig.json │ │ │ └── webpack.config.js │ │ └── tutorial.md │ └── helloworld/ │ ├── .gitignore │ ├── README.md │ ├── client.js │ ├── debugging/ │ │ └── node-client.js │ ├── envoy.yaml │ ├── helloworld.proto │ ├── index.html │ ├── package.json │ └── server.js ├── packages/ │ └── grpc-web/ │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── docker/ │ │ └── jsunit-test/ │ │ └── Dockerfile │ ├── exports.js │ ├── externs.js │ ├── gulpfile.js │ ├── index.d.ts │ ├── package.json │ ├── protractor.conf.js │ ├── protractor_spec.js │ ├── scripts/ │ │ ├── build.js │ │ ├── common.py │ │ ├── gen_all_tests_js.py │ │ ├── gen_test_htmls.py │ │ ├── generate_test_files.sh │ │ ├── run_jsunit_tests.sh │ │ ├── template_all_tests_js.txt │ │ └── template_test_html.txt │ └── test/ │ ├── closure_client.js │ ├── common.js │ ├── eval_test.js │ ├── export_test.js │ ├── generated_code_test.js │ ├── gulpfile.js │ ├── plugin_test.js │ ├── protos/ │ │ ├── echo.proto │ │ ├── foo.proto │ │ ├── models.proto │ │ ├── myapi/ │ │ │ └── v1/ │ │ │ ├── myapi-two.proto │ │ │ └── myapi.proto │ │ ├── nopackage.proto │ │ ├── otherapi/ │ │ │ └── v1/ │ │ │ └── otherapi.proto │ │ ├── test01.proto │ │ ├── test02.proto │ │ └── test03.proto │ ├── tsc-tests/ │ │ ├── client01.ts │ │ ├── client02.ts │ │ ├── client03.ts │ │ ├── client04.ts │ │ ├── client05.ts │ │ └── client06.ts │ └── tsc_test.js ├── scripts/ │ ├── README.md │ ├── docker-run-build-tests.sh │ ├── docker-run-interop-tests.sh │ ├── docker-run-jsunit-tests.sh │ ├── docker-run-mocha-tests.sh │ ├── init_submodules.sh │ ├── kokoro.sh │ ├── release_notes.py │ ├── run_basic_tests.sh │ ├── run_interop_tests.sh │ └── test-proxy.sh ├── src/ │ └── proto/ │ └── grpc/ │ └── testing/ │ ├── empty.proto │ ├── messages.proto │ └── test.proto └── test/ └── interop/ ├── .gitignore ├── README.md ├── envoy.yaml ├── index.html ├── interop_client.js ├── package.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bazelci/presubmit.yml ================================================ --- # TODO(yannic): Enable buildifier and test on Windows and RBE (both unsupported by rules_closure). platforms: macos: build_targets: - //... test_targets: - //... ubuntu1804: build_targets: - //... test_targets: - //... ================================================ FILE: .bazelignore ================================================ # //third_party conatins git submodules. third_party/ ================================================ FILE: .bazelrc ================================================ # Common build settings for unix-like systems build --copt=-Wno-error=deprecated-declarations build --host_copt=-Wno-error=deprecated-declarations # Required until this is the default; expected in Bazel 7 common --enable_bzlmod # Per-user settings (gitignored). Keep last so local flags can override. # Docs: https://bazel.build/configure/best-practices#bazelrc try-import %workspace%/.bazelrc.user ================================================ FILE: .bazelrc.windows ================================================ # This is a Windows-specific bazelrc file for use in GitHub Actions. # It contains settings from the main .bazelrc file that are relevant for Windows builds. # Windows specific settings build --copt=-DABSL_HAVE_WORKING_GCC_WNO_DEPRECATED_DECLARATIONS=0 build --host_copt=-DABSL_HAVE_WORKING_GCC_WNO_DEPRECATED_DECLARATIONS=0 # Required until this is the default; expected in Bazel 7 common --enable_bzlmod # Per-user settings (gitignored). Keep last so local flags can override. # Docs: https://bazel.build/configure/best-practices#bazelrc try-import %workspace%/.bazelrc.user ================================================ FILE: .dockerignore ================================================ **/dist **/node_modules packages/grpc-web/generated ================================================ FILE: .github/workflows/make-plugin-linux.yml ================================================ name: Make Linux Plugin on: push: paths: - .github/workflows/make-plugin-linux.yml pull_request: paths: - .github/workflows/make-plugin-linux.yml workflow_dispatch: inputs: version_number: description: 'Version number' required: true default: '2.x.x' jobs: build: strategy: fail-fast: false matrix: os: [ubuntu-22.04, ubuntu-22.04-arm] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Compute VERSION_NUMBER run: | INPUT_VERSION="${{ github.event.inputs.version_number }}" if [ -n "$INPUT_VERSION" ]; then VERSION="$INPUT_VERSION" else VERSION="$GITHUB_REF_NAME" fi VERSION="${VERSION//\//-}" echo "VERSION_NUMBER=$VERSION" >> "$GITHUB_ENV" echo "Computed VERSION_NUMBER=$VERSION" - name: Compute ARCH suffix and artifact name id: meta run: | ARCH=$(uname -m) case "$ARCH" in aarch64|arm64) ARCH_SUFFIX="aarch64" ;; x86_64|amd64) ARCH_SUFFIX="x86_64" ;; *) echo "Unsupported architecture: $ARCH" >&2 exit 1 ;; esac ARTIFACT="protoc-gen-grpc-web-${VERSION_NUMBER}-linux-${ARCH_SUFFIX}" echo "ARTIFACT=$ARTIFACT" >> "$GITHUB_ENV" echo "artifact=$ARTIFACT" >> "$GITHUB_OUTPUT" echo "Will produce artifact: $ARTIFACT" - name: Install Bazelisk (Bazel) run: | sudo apt-get update sudo apt-get install -y unzip zip ARCH=$(uname -m) case "$ARCH" in aarch64|arm64) BAZELISK_URL="https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-arm64" ;; x86_64|amd64) BAZELISK_URL="https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64" ;; *) echo "Unsupported architecture for Bazelisk: $ARCH" >&2 exit 1 ;; esac echo "Downloading Bazelisk from $BAZELISK_URL" sudo curl -L -o /usr/local/bin/bazelisk "$BAZELISK_URL" sudo chmod +x /usr/local/bin/bazelisk # Also provide `bazel` symlink for tools that expect it sudo ln -sf /usr/local/bin/bazelisk /usr/local/bin/bazel bazelisk version - name: Build protoc-gen-grpc-web with Bazel (older glibc baseline) run: | # Partially static link libstdc++/libgcc to reduce GLIBCXX constraints bazelisk build \ --linkopt=-static-libstdc++ \ --linkopt=-static-libgcc \ //javascript/net/grpc/web/generator:protoc-gen-grpc-web - name: Move artifact run: | mv bazel-bin/javascript/net/grpc/web/generator/protoc-gen-grpc-web \ ./${ARTIFACT} - name: Generate sha256 run: | openssl dgst -sha256 -r -out ${ARTIFACT}.sha256 \ ${ARTIFACT} - name: Verify sha256 run: sha256sum -c ${ARTIFACT}.sha256 - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: ${{ steps.meta.outputs.artifact }} path: protoc-gen-grpc-web* ================================================ FILE: .github/workflows/make-plugin-mac-os.yml ================================================ name: Make macOS Plugin on: push: paths: - .github/workflows/make-plugin-mac-os.yml pull_request: paths: - .github/workflows/make-plugin-mac-os.yml workflow_dispatch: inputs: version_number: description: 'Version number' required: true default: '2.x.x' jobs: build: strategy: fail-fast: false matrix: os: [macos-13, macos-14] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Compute VERSION_NUMBER run: | INPUT_VERSION="${{ github.event.inputs.version_number }}" if [ -n "$INPUT_VERSION" ]; then VERSION="$INPUT_VERSION" else VERSION="$GITHUB_REF_NAME" fi # Minimal sanitization: replace slashes with dashes to keep filenames valid VERSION="${VERSION//\//-}" echo "VERSION_NUMBER=$VERSION" >> "$GITHUB_ENV" echo "Computed VERSION_NUMBER=$VERSION" - name: Compute ARCH suffix and artifact name id: meta run: | ARCH=$(uname -m) case "$ARCH" in arm64) ARCH_SUFFIX="aarch64" ;; x86_64) ARCH_SUFFIX="x86_64" ;; *) echo "Unsupported architecture: $ARCH" >&2 exit 1 ;; esac ARTIFACT="protoc-gen-grpc-web-${VERSION_NUMBER}-darwin-${ARCH_SUFFIX}" echo "ARTIFACT=$ARTIFACT" >> "$GITHUB_ENV" echo "artifact=$ARTIFACT" >> "$GITHUB_OUTPUT" echo "Will produce artifact: $ARTIFACT" - name: Install Bazelisk (Bazel) run: | brew update brew install bazelisk bazelisk version - name: Build protoc-gen-grpc-web with Bazel run: | bazelisk build //javascript/net/grpc/web/generator:protoc-gen-grpc-web - name: Move artifact run: | mv bazel-bin/javascript/net/grpc/web/generator/protoc-gen-grpc-web \ ./${ARTIFACT} - name: Generate sha256 run: | openssl dgst -sha256 -r -out ${ARTIFACT}.sha256 \ ${ARTIFACT} - name: Verify sha256 run: shasum -a 256 -c ${ARTIFACT}.sha256 - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: ${{ steps.meta.outputs.artifact }} path: protoc-gen-grpc-web* ================================================ FILE: .github/workflows/make-plugin-windows.yml ================================================ name: Make Windows Plugin on: push: paths: - .github/workflows/make-plugin-windows.yml pull_request: paths: - .github/workflows/make-plugin-windows.yml workflow_dispatch: inputs: version_number: description: 'Version number' required: true default: '2.x.x' jobs: build: strategy: fail-fast: false matrix: os: [windows-2025, windows-11-arm] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Compute VERSION_NUMBER run: | INPUT_VERSION="${{ github.event.inputs.version_number }}" if [ -n "$INPUT_VERSION" ]; then VERSION="$INPUT_VERSION" else VERSION="$GITHUB_REF_NAME" fi # Minimal sanitization: replace slashes with dashes to keep filenames valid VERSION="${VERSION//\//-}" echo "VERSION_NUMBER=$VERSION" >> "$GITHUB_ENV" echo "Computed VERSION_NUMBER=$VERSION" shell: bash - name: Compute ARCH suffix and artifact name id: meta shell: powershell run: | $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture switch ($arch) { 'Arm64' { $archSuffix = 'aarch64' } 'X64' { $archSuffix = 'x86_64' } default { Write-Error "Unsupported architecture: $arch"; exit 1 } } $artifact = "protoc-gen-grpc-web-$($env:VERSION_NUMBER)-windows-$archSuffix.exe" "ARTIFACT=$artifact" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "artifact=$artifact" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append Write-Host "Will produce artifact: $artifact" - name: Install Bazelisk (Bazel) shell: powershell run: | $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture switch ($arch) { 'Arm64' { $suffix = 'arm64' } 'X64' { $suffix = 'amd64' } default { Write-Error "Unsupported architecture: $arch"; exit 1 } } $url = "https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-windows-$suffix.exe" $destDir = "$env:RUNNER_TEMP" $dest = Join-Path $destDir 'bazelisk.exe' Write-Host "Downloading Bazelisk from $url to $dest" Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $dest # Add to PATH for subsequent steps $destDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Print Bazelisk version shell: powershell run: bazelisk version - name: Build protoc-gen-grpc-web with Bazel run: bazelisk --noworkspace_rc --bazelrc=.bazelrc.windows build //javascript/net/grpc/web/generator:protoc-gen-grpc-web shell: powershell - name: Move artifact run: | mv bazel-bin/javascript/net/grpc/web/generator/protoc-gen-grpc-web.exe "./${ARTIFACT}" shell: bash - name: Generate sha256 run: | openssl dgst -sha256 -r -out "${ARTIFACT}.sha256" "${ARTIFACT}" shell: bash - name: Verify sha256 shell: powershell run: | $shaFile = "${env:ARTIFACT}.sha256" if (-not (Test-Path $shaFile)) { Write-Error "SHA256 file not found: $shaFile"; exit 1 } $line = Get-Content -Raw $shaFile if (-not $line) { Write-Error "Empty sha256 file: $shaFile"; exit 1 } $expected = ($line -split '\s+')[0].ToLower() $actual = (Get-FileHash "${env:ARTIFACT}" -Algorithm SHA256).Hash.ToLower() if ($actual -ne $expected) { Write-Error "SHA256 mismatch. Expected $expected, got $actual" exit 1 } else { Write-Host "SHA256 verified: $actual" } - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: ${{ steps.meta.outputs.artifact }} path: protoc-gen-grpc-web* ================================================ FILE: .github/workflows/release-source-archive.yml ================================================ name: Publish Stable Source Archive on: release: types: [published] jobs: # Whenever a release is published, this uploads an accompanying stable source archive. # # Github doesn't guarantee stability of source archives for more than 6 months[1]. # More stability is required by projects like Bazel Central Registry[2][3]. # # [1]: https://github.blog/open-source/git/update-on-the-future-stability-of-source-code-archives-and-hashes/ # [2]: https://github.com/bazelbuild/bazel-central-registry/blob/main/docs/README.md#validations # [3]: https://blog.bazel.build/2023/02/15/github-archive-checksum.html bazel-release-archive: defaults: run: # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ shell: /usr/bin/bash -euxo pipefail {0} env: # github.ref_name is defined here: # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context TAG: ${{github.ref_name}} runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v3 # GITHUB_REF is defined here: # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables - run: git archive --format zip --prefix "grpc-web-$TAG/" --output "grpc-web-source-${TAG}.zip" "$GITHUB_REF" - run: git archive --format tar.gz --prefix "grpc-web-$TAG/" --output "grpc-web-source-${TAG}.tar.gz" "$GITHUB_REF" - run: gh release upload "${TAG}" "grpc-web-source-${TAG}.zip" "grpc-web-source-${TAG}.tar.gz" env: GH_TOKEN: ${{ github.token }} ================================================ FILE: .gitignore ================================================ .vscode bazel-bin bazel-genfiles bazel-grpc-web bazel-out bazel-testlogs *.o protoc-gen-* .DS_Store target .project .classpath .settings zig-out zig-cache .zig-cache ================================================ FILE: .gitmodules ================================================ [submodule "third_party/protobuf"] path = third_party/protobuf url = https://github.com/protocolbuffers/protobuf.git ================================================ FILE: AUTHORS ================================================ Google Inc. ================================================ FILE: CHANGELOG.md ================================================ [//]: # (GENERATED FILE -- DO NOT EDIT!) [//]: # (See scripts/release_notes.py for more details.) ## 2.0.2 - [#1507](https://github.com/grpc/grpc-web/pull/1507) Use regular enums for compatibility with TypeScript 5.9+ ## 2.0.1 - [#1501](https://github.com/grpc/grpc-web/pull/1501) Update workflow to use Ubuntu 22.04 and static link libstdc++/libgcc ## 2.0.0 ### Major Features - [#1490](https://github.com/grpc/grpc-web/pull/1490) Enable support for Editions in protoc-gen-grpc-web. @Quarke - [#1445](https://github.com/grpc/grpc-web/pull/1445) Upgrade protobuf to 27.1 and modernize codegen using new APIs @benjaminp ### Other Changes - [#1496](https://github.com/grpc/grpc-web/pull/1496) Rework release workflows using Github runners (replacing Zig) - [#1494](https://github.com/grpc/grpc-web/pull/1494) Updates Closure compiler (`20250820.0.0`) and library - [#1463](https://github.com/grpc/grpc-web/pull/1463) fix: format of typescript definition @nnnnoel - [#1456](https://github.com/grpc/grpc-web/pull/1456) Bazel bzlmod support for grpc-web. @gonzojive - [#1452](https://github.com/grpc/grpc-web/pull/1452) Update protobuf-JS `3.14.0` -> `3.21.4` - [#1441](https://github.com/grpc/grpc-web/pull/1441) Upgrade to Bazel 6.5.0 ## 1.5.0 - [#1369](https://github.com/grpc/grpc-web/pull/1369) (Typescript) Mark some `metadata` parameters as optional @andrewmbenton - [#1335](https://github.com/grpc/grpc-web/pull/1335) Update Debian (and other deps) and remove Java In-process Proxy - [#1334](https://github.com/grpc/grpc-web/pull/1334) Allow mixed-case headers - [#1330](https://github.com/grpc/grpc-web/pull/1330) Update ES6 .d.ts imports with comment about corresponding proto import... @gonzojive - [#1313](https://github.com/grpc/grpc-web/pull/1313) Update ES6 imports with comment about corresponding proto import path. @reddaly ## 1.4.2 - [#1289](https://github.com/grpc/grpc-web/pull/1289) Expose getName() in MethodDescriptor and fix TS definitions. - [#1230](https://github.com/grpc/grpc-web/pull/1230) GrpcWebClientReadableStream: keep falsy data @pro-wh ## 1.4.1 - [#1286](https://github.com/grpc/grpc-web/pull/1286) Fix duplicate dot in enum name (when "package" is specified) ## 1.4.0 ### Major Features - [#1249](https://github.com/grpc/grpc-web/pull/1249) Use Zig to build aarch64 binaries @hronro - [#1203](https://github.com/grpc/grpc-web/pull/1203) Github Actions (workflows) for building `protoc-gen-grpc-web` plugins ### Other Changes - [#1279](https://github.com/grpc/grpc-web/pull/1279) Fixes the status codes ordering in typescript definitions @chandraaditya - [#1278](https://github.com/grpc/grpc-web/pull/1278) Fix Enum with module in generated TS interface. - [#1254](https://github.com/grpc/grpc-web/pull/1254) Remove Trailing Slashes from Hostname @jkjk822 - [#1252](https://github.com/grpc/grpc-web/pull/1252) Fix Zig setup step in CI @hronro - [#1231](https://github.com/grpc/grpc-web/pull/1231) Add version flag and version info in generated code @meling - [#1225](https://github.com/grpc/grpc-web/pull/1225) Improve error message & Internal code sync - [#1222](https://github.com/grpc/grpc-web/pull/1222) Update envoy version to 1.22 (with config updates) @tomk9 - [#1211](https://github.com/grpc/grpc-web/pull/1211) Upgrade protobuf and grpc deps @aapeliv - [#1199](https://github.com/grpc/grpc-web/pull/1199) Revert "Expose MethodDescriptor's public methods" ## 1.3.1 - [#1184](https://github.com/grpc/grpc-web/pull/1184) Correctly support proto3 optional fields in commonjs+dts .d.ts output @mattnathan - [#1173](https://github.com/grpc/grpc-web/pull/1173) Update envoy version to 1.20 - [#1172](https://github.com/grpc/grpc-web/pull/1172) Fix issue where **no RPC is issued when `deadline` is specified.** - [#1167](https://github.com/grpc/grpc-web/pull/1167) Fix missing TypeScript return type for `serverStreaming` calls. @lukasmoellerch - [#1166](https://github.com/grpc/grpc-web/pull/1166) Add missing exports from `RpcError` and add test. - [#1164](https://github.com/grpc/grpc-web/pull/1164) Add missing class exports @tinrab - [#1160](https://github.com/grpc/grpc-web/pull/1160) Expose MethodDescriptor's public methods @tomferreira ## 1.3.0 ### Major Features - [#1139](https://github.com/grpc/grpc-web/pull/1139) Improve error type with `RpcError` & internal code sync (contributor: @TomiBelan) + (experimental) Typescript users need to update type references from `Error` -> `RpcError` ### Other Changes - [#1140](https://github.com/grpc/grpc-web/pull/1140) Improve `RpcError.code` typing & internal code sync (contributor: @richieforeman) - [#1138](https://github.com/grpc/grpc-web/pull/1138) Remove Bazel in Javascript toolchain - [#1137](https://github.com/grpc/grpc-web/pull/1137) Revamp Closure JsUnit tests runtime and optimize test/build flows. - [#1115](https://github.com/grpc/grpc-web/pull/1115) Bump Bazel version -> 4.1.0 and Protobuf version -> 3.17.3 - [#1107](https://github.com/grpc/grpc-web/pull/1107) Allow for custom install prefix @06kellyjac - [#1063](https://github.com/grpc/grpc-web/pull/1063) Also set timeout on HTTP request if deadline for grpc call is set @Yannic - [#1004](https://github.com/grpc/grpc-web/pull/1004) Bump closure library version to v20201102 - [#1002](https://github.com/grpc/grpc-web/pull/1002) Bump Envoy version to 1.16.1 - [#998](https://github.com/grpc/grpc-web/pull/998) Fix GrpcWebClientBaseOptions types in index.d.ts @acalvo - [#971](https://github.com/grpc/grpc-web/pull/971) Add grpc.web.ClientOptions to better document options and add type res... @jennnnny - [#969](https://github.com/grpc/grpc-web/pull/969) Fix non-determinism in code generator - [#941](https://github.com/grpc/grpc-web/pull/941) Fix Protobuf .d.ts typings for .proto files without package @Yannic ## 1.2.1 - [#910](https://github.com/grpc/grpc-web/pull/910) Add test to show how to access metadata in interceptor - [#903](https://github.com/grpc/grpc-web/pull/903) Add error handling to a few error conditions - [#886](https://github.com/grpc/grpc-web/pull/886) Add missing types definitions - [#885](https://github.com/grpc/grpc-web/pull/885) Bump Envoy to 1.15.0 - [#884](https://github.com/grpc/grpc-web/pull/884) Update protoc plugin to support Proto3 optional - [#882](https://github.com/grpc/grpc-web/pull/882) Add @interface MethodDescroptorInterface [@Jennnnny](https://github.com/Jennnnny) - [#880](https://github.com/grpc/grpc-web/pull/880) Update Bazel to 3.3.1 [@Yannic](https://github.com/Yannic) - [#874](https://github.com/grpc/grpc-web/pull/874) Add removeListener and missing metadata event types [@danielthank](https://github.com/danielthank) - [#872](https://github.com/grpc/grpc-web/pull/872) [bazel] Introduce grpc_web_toolchain [@Yannic](https://github.com/Yannic) - [#871](https://github.com/grpc/grpc-web/pull/871) [generator] Refactor dependency management [@Yannic](https://github.com/Yannic) - [#869](https://github.com/grpc/grpc-web/pull/869) Add scripts to run interop-tests on grpc-web Java connector ## 1.2.0 ### Major Features - [#847](https://github.com/grpc/grpc-web/pull/847) Allow multiple .on() callbacks and fix issue with non-OK status ### Other Changes - [#859](https://github.com/grpc/grpc-web/pull/859) Fix envoy.yaml deprecated fields [@dmaixner](https://github.com/dmaixner) - [#858](https://github.com/grpc/grpc-web/pull/858) Refactor error handling in grpcwebclientbase - [#857](https://github.com/grpc/grpc-web/pull/857) Migrate to ES6 classes - [#852](https://github.com/grpc/grpc-web/pull/852) Update to use @grpc/grpc-js node package, and update helloworld exampl... - [#851](https://github.com/grpc/grpc-web/pull/851) Add a ThenableCall base class for the promise-based unaryCall function [@Jennnnny](https://github.com/Jennnnny) - [#844](https://github.com/grpc/grpc-web/pull/844) Fix code generator bug and add tests - [#833](https://github.com/grpc/grpc-web/pull/833) Add proper author attribution to release notes / changelog - [#827](https://github.com/grpc/grpc-web/pull/827) Splitting callback based client and Promise based client into multiple... [@Jennnnny](https://github.com/Jennnnny) - [#822](https://github.com/grpc/grpc-web/pull/822) use explicit envoy release tag [@xsbchen](https://github.com/xsbchen) - [#821](https://github.com/grpc/grpc-web/pull/821) Experimental Feature: Add ES6 import style [@Yannic](https://github.com/Yannic) - [#738](https://github.com/grpc/grpc-web/pull/738) Avoid double slash in url when client hostname has tailing slash [@hanabi1224](https://github.com/hanabi1224) ## 1.1.0 ### Major Features - [#785](https://github.com/grpc/grpc-web/pull/785) grpc-web interceptors implementation [@Jennnnny](https://github.com/Jennnnny) - [#772](https://github.com/grpc/grpc-web/pull/772) Add interop test spec and interop tests ### Other Changes - [#818](https://github.com/grpc/grpc-web/pull/818) All java connector interop tests are passing now - [#804](https://github.com/grpc/grpc-web/pull/804) Fix a bug in test: callback not properly intercepted - [#801](https://github.com/grpc/grpc-web/pull/801) Trying to speed up tests - [#797](https://github.com/grpc/grpc-web/pull/797) Split basic tests with interop tests - [#780](https://github.com/grpc/grpc-web/pull/780) Add missing separator to imports from external files [@tomiaijo](https://github.com/tomiaijo) - [#777](https://github.com/grpc/grpc-web/pull/777) Add .on(metadata,...) callback to distinguish initial metadata - [#764](https://github.com/grpc/grpc-web/pull/764) [generator] Move options parsing into dedicated class [@Yannic](https://github.com/Yannic) - [#761](https://github.com/grpc/grpc-web/pull/761) Update generic client [@Jennnnny](https://github.com/Jennnnny) - [#756](https://github.com/grpc/grpc-web/pull/756) Add eval test for TypeScript generated code - [#752](https://github.com/grpc/grpc-web/pull/752) Disable static checkers on generated js files [@IagoLast](https://github.com/IagoLast) - [#747](https://github.com/grpc/grpc-web/pull/747) Enable builder pattern in Typescript protobuf messages. [@Orphis](https://github.com/Orphis) - [#746](https://github.com/grpc/grpc-web/pull/746) Generate Promise based overloads for unary calls in Typescript [@Orphis](https://github.com/Orphis) - [#745](https://github.com/grpc/grpc-web/pull/745) [bazel] Update rules_closure + fix linter warnings [@Yannic](https://github.com/Yannic) - [#734](https://github.com/grpc/grpc-web/pull/734) Allow GrpcWebStreamParser to accept Uint8Array [@travikk](https://github.com/travikk) - [#723](https://github.com/grpc/grpc-web/pull/723) Update bazel version - [#720](https://github.com/grpc/grpc-web/pull/720) Fix grpcwebproxy interop - [#716](https://github.com/grpc/grpc-web/pull/716) allow_origin is deprecated in latest envoy server [@noconnor](https://github.com/noconnor) - [#695](https://github.com/grpc/grpc-web/pull/695) Fix issue 632 (double execution of callback) [@hfinger](https://github.com/hfinger) - [#692](https://github.com/grpc/grpc-web/pull/692) Do not hardcode CXX to g++ ## 1.0.7 - [#671](https://github.com/grpc/grpc-web/pull/671) Add metadata to error callback - [#668](https://github.com/grpc/grpc-web/pull/668) Remove stream_body.proto - [#665](https://github.com/grpc/grpc-web/pull/665) Add config for Bazel CI [@Yannic](https://github.com/Yannic) - [#663](https://github.com/grpc/grpc-web/pull/663) nginx example Expose-Headers add Grpc-Message,Grpc-Status [@zsluedem](https://github.com/zsluedem) - [#657](https://github.com/grpc/grpc-web/pull/657) Ensure that the end callback is called [@vbfox](https://github.com/vbfox) - [#655](https://github.com/grpc/grpc-web/pull/655) Use closure compiler from npm in build.js [@vbfox](https://github.com/vbfox) - [#654](https://github.com/grpc/grpc-web/pull/654) Ignore MacOS .DS_Store files [@vbfox](https://github.com/vbfox) - [#652](https://github.com/grpc/grpc-web/pull/652) Fix error callback - [#644](https://github.com/grpc/grpc-web/pull/644) Add CallOptions class [@Jennnnny](https://github.com/Jennnnny) - [#641](https://github.com/grpc/grpc-web/pull/641) Add/update GOVERNANCE.md and CONTRIBUTING.md - [#635](https://github.com/grpc/grpc-web/pull/635) Fix generated code return type, and remove unused var - [#628](https://github.com/grpc/grpc-web/pull/628) Added API for simple unary call [@Jennnnny](https://github.com/Jennnnny) - [#621](https://github.com/grpc/grpc-web/pull/621) Fix output directory name when using import_style=typescript [@asv](https://github.com/asv) - [#619](https://github.com/grpc/grpc-web/pull/619) Return specific grpc status code on http error [@harmangakhal](https://github.com/harmangakhal) - [#618](https://github.com/grpc/grpc-web/pull/618) Generate method descriptors into multiple files [@Jennnnny](https://github.com/Jennnnny) - [#617](https://github.com/grpc/grpc-web/pull/617) Remove `enabled` deprecated field [@gsalisi](https://github.com/gsalisi) - [#615](https://github.com/grpc/grpc-web/pull/615) Add support in code generator for printing only method descriptors [@Jennnnny](https://github.com/Jennnnny) - [#608](https://github.com/grpc/grpc-web/pull/608) Fix status and error callbacks ## 1.0.6 - [#604](https://github.com/grpc/grpc-web/pull/604) Add option to set withCredentials to true - [#603](https://github.com/grpc/grpc-web/pull/603) Adding some groundwork for generic client [@Jennnnny](https://github.com/Jennnnny) - [#600](https://github.com/grpc/grpc-web/pull/600) Add generated code eval test - [#599](https://github.com/grpc/grpc-web/pull/599) fix wrong package name of input type [@lqs](https://github.com/lqs) - [#593](https://github.com/grpc/grpc-web/pull/593) Fix: Helloworld Example - Enabled Deprecation [@gary-lo](https://github.com/gary-lo) ## 1.0.5 - [#582](https://github.com/grpc/grpc-web/pull/582) Ensure credentials are not undefined in typescript [@Globegitter](https://github.com/Globegitter) - [#579](https://github.com/grpc/grpc-web/pull/579) Uppercase enum keys in TypeScript definitions [@benfoxbotica](https://github.com/benfoxbotica) - [#578](https://github.com/grpc/grpc-web/pull/578) Fix depset issues with/upgrade to Bazel 0.27.1 [@factuno-db](https://github.com/factuno-db) - [#567](https://github.com/grpc/grpc-web/pull/567) Introducing MethodDescriptor [@Jennnnny](https://github.com/Jennnnny) - [#559](https://github.com/grpc/grpc-web/pull/559) Adding new fields to MethodInfo [@Jennnnny](https://github.com/Jennnnny) - [#556](https://github.com/grpc/grpc-web/pull/556) Add fix for deadline of strings, NaN, Infinity and -Infinity [@CatEars](https://github.com/CatEars) - [#546](https://github.com/grpc/grpc-web/pull/546) Changes to deserializeBinary API - [#540](https://github.com/grpc/grpc-web/pull/540) Method Derserializer should take Uint8Array [@pnegahdar](https://github.com/pnegahdar) - [#519](https://github.com/grpc/grpc-web/pull/519) remove duplicated has$field$ method for oneof [@yangjian](https://github.com/yangjian) - [#512](https://github.com/grpc/grpc-web/pull/512) Make client args `credentials` and `options` optional [@jonahbron](https://github.com/jonahbron) ## 1.0.4 - [#502](https://github.com/grpc/grpc-web/pull/502) Attempt to fix flakiness of 'bazel test' [@Yannic](https://github.com/Yannic) - [#497](https://github.com/grpc/grpc-web/pull/497) Remove a return that skip emission of end callback [@tinou98](https://github.com/tinou98) - [#494](https://github.com/grpc/grpc-web/pull/494) [bazel] Migrate protobuf info provider to new-style one [@Yannic](https://github.com/Yannic) - [#482](https://github.com/grpc/grpc-web/pull/482) feature: Typings codegen for bytes field type [@shaxbee](https://github.com/shaxbee) - [#481](https://github.com/grpc/grpc-web/pull/481) Add module alias to enums for Typescript [@rogchap](https://github.com/rogchap) - [#460](https://github.com/grpc/grpc-web/pull/460) add typescript definition for Oneof fields [@yangjian](https://github.com/yangjian) - [#452](https://github.com/grpc/grpc-web/pull/452) fix: exclude map entry message from typings, fix optional values [@shaxbee](https://github.com/shaxbee) - [#448](https://github.com/grpc/grpc-web/pull/448) Export Map types correctly, optional getter/setters for message types [@shaxbee](https://github.com/shaxbee) - [#444](https://github.com/grpc/grpc-web/pull/444) feature: Messages in typings extending jspb.Message [@shaxbee](https://github.com/shaxbee) - [#433](https://github.com/grpc/grpc-web/pull/433) Match name nesting and imports in .d.ts with .js files [@shaxbee](https://github.com/shaxbee) - [#430](https://github.com/grpc/grpc-web/pull/430) Use camelCase in AsObject definition [@johanbrandhorst](https://github.com/johanbrandhorst) - [#429](https://github.com/grpc/grpc-web/pull/429) Fix type error in serverStreaming method [@johanbrandhorst](https://github.com/johanbrandhorst) - [#427](https://github.com/grpc/grpc-web/pull/427) Promise function should use ES5 functions rather than fat arrows (IE b... [@rogchap](https://github.com/rogchap) - [#422](https://github.com/grpc/grpc-web/pull/422) Enable ADVANCED_OPTIMIZATIONS in Closure Compiler [@jjbubudi](https://github.com/jjbubudi) - [#421](https://github.com/grpc/grpc-web/pull/421) [bazel] Upgrade to 0.22.0 [@Yannic](https://github.com/Yannic) - [#413](https://github.com/grpc/grpc-web/pull/413) Emit status event on empty stream response [@shaxbee](https://github.com/shaxbee) - [#409](https://github.com/grpc/grpc-web/pull/409) Fix metadata typings for TS client [@bpicolo](https://github.com/bpicolo) - [#404](https://github.com/grpc/grpc-web/pull/404) Generate Typescript definition for top level Enums [@rogchap](https://github.com/rogchap) ## 1.0.3 - [#391](https://github.com/grpc/grpc-web/pull/391) A script to compile protoc plugin - [#385](https://github.com/grpc/grpc-web/pull/385) Codegen: Support nested types and enums [@shaxbee](https://github.com/shaxbee) - [#368](https://github.com/grpc/grpc-web/pull/368) Make the bazel rules work with current rules_closure. [@factuno-db](https://github.com/factuno-db) - [#367](https://github.com/grpc/grpc-web/pull/367) update examples to use addService [@mitchdraft](https://github.com/mitchdraft) - [#365](https://github.com/grpc/grpc-web/pull/365) Fix response header value with colon - [#362](https://github.com/grpc/grpc-web/pull/362) Fix the method name clashes for generated commonjs files [@weilip1803](https://github.com/weilip1803) - [#360](https://github.com/grpc/grpc-web/pull/360) Fix the import path for generated typescript files [@at-ishikawa](https://github.com/at-ishikawa) ## 1.0.2 ## 1.0.1 - [#354](https://github.com/grpc/grpc-web/pull/354) [dts] Generate PromiseClient type definitions in d.ts file [@at-ishikawa](https://github.com/at-ishikawa) - [#352](https://github.com/grpc/grpc-web/pull/352) Add a max grpc timeout to the echo example. [@mjduijn](https://github.com/mjduijn) - [#348](https://github.com/grpc/grpc-web/pull/348) Fix output dts about 'repeated' for --grpc-web_out=import_style=common... [@rybbchao](https://github.com/rybbchao) - [#345](https://github.com/grpc/grpc-web/pull/345) update typescript generation to work in strict mode [@henriiik](https://github.com/henriiik) - [#330](https://github.com/grpc/grpc-web/pull/330) Use official rules_closure repository [@Yannic](https://github.com/Yannic) ## 1.0.0 - [#314](https://github.com/grpc/grpc-web/pull/314) Add a unit test for proto with no package - [#313](https://github.com/grpc/grpc-web/pull/313) Show how deadline can be set - [#311](https://github.com/grpc/grpc-web/pull/311) Document how to prevent Envoy to timeout streaming [@mitar](https://github.com/mitar) - [#310](https://github.com/grpc/grpc-web/pull/310) Correctly generate code if package name is empty [@mitar](https://github.com/mitar) - [#304](https://github.com/grpc/grpc-web/pull/304) Add a simple Hello World Guide - [#303](https://github.com/grpc/grpc-web/pull/303) Error code should be number - [#276](https://github.com/grpc/grpc-web/pull/276) Fix plugin compile error - [#272](https://github.com/grpc/grpc-web/pull/272) Fix cpp warnings ## 0.4.0 - [#263](https://github.com/grpc/grpc-web/pull/263) Make "Quick" start quicker - [#258](https://github.com/grpc/grpc-web/pull/258) Experimental Typescript support - [#257](https://github.com/grpc/grpc-web/pull/257) Fix bug with button in example ## 0.3.0 - [#249](https://github.com/grpc/grpc-web/pull/249) Various fixes to codegen plugin - [#247](https://github.com/grpc/grpc-web/pull/247) Add generated code unit test - [#240](https://github.com/grpc/grpc-web/pull/240) webpack demo - [#239](https://github.com/grpc/grpc-web/pull/239) Expose response metadata for unary calls - [#219](https://github.com/grpc/grpc-web/pull/219) Add bazel rule closure_grpc_web_library [@Yannic](https://github.com/Yannic) - [#217](https://github.com/grpc/grpc-web/pull/217) Added multiple proxies interoperability ## 0.2.0 - [#212](https://github.com/grpc/grpc-web/pull/212) Added commonjs-example Dockerfile - [#211](https://github.com/grpc/grpc-web/pull/211) commonjs support with import_style option [@zaucy](https://github.com/zaucy) - [#210](https://github.com/grpc/grpc-web/pull/210) grpcweb npm runtime module [@zaucy](https://github.com/zaucy) - [#209](https://github.com/grpc/grpc-web/pull/209) Add bazel integration and tests - [#206](https://github.com/grpc/grpc-web/pull/206) Surface underlying XHR errors better - [#185](https://github.com/grpc/grpc-web/pull/185) Support for proto files without packages [@zaucy](https://github.com/zaucy) ================================================ FILE: CODE-OF-CONDUCT.md ================================================ ## Community Code of Conduct gRPC-Web follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). ================================================ FILE: CONTRIBUTING.md ================================================ # How to contribute We definitely welcome patches and contribution to gRPC-Web! Here is some guideline and information about how to do so. Please read the gRPC organization's [governance rules](https://github.com/grpc/grpc-community/blob/master/governance.md) and [contribution guidelines](https://github.com/grpc/grpc-community/blob/master/CONTRIBUTING.md) before proceeding. ## Getting started ### Legal requirements In order to protect both you and ourselves, you will need to sign the [Contributor License Agreement](https://cla.developers.google.com/clas). ### Technical requirements The basic build script should run to completion. ```sh $ ./scripts/kokoro.sh ``` More details to come. ================================================ FILE: GOVERNANCE.md ================================================ This repository is governed by the gRPC organization's [governance rules](https://github.com/grpc/grpc-community/blob/master/governance.md). ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS.md ================================================ This page lists all active maintainers of this repository. If you were a maintainer and would like to add your name to the Emeritus list, please send us a PR. See [GOVERNANCE.md](https://github.com/grpc/grpc-community/blob/master/governance.md) for governance guidelines and how to become a maintainer. See [CONTRIBUTING.md](https://github.com/grpc/grpc-community/blob/master/CONTRIBUTING.md) for general contribution guidelines. ## Maintainers (in alphabetical order) - [sampajano](https://github.com/sampajano), Google Inc. - [wenbozhu](https://github.com/wenbozhu), Google Inc. ## Emeritus Maintainers (in alphabetical order) - [fengli79](https://github.com/fengli79) - [stanley-cheung](https://github.com/stanley-cheung) ================================================ FILE: MODULE.bazel ================================================ """ A bazel module for the grpc-web project. Visit https://grpc.io/ and https://github.com/grpc/grpc-web for more information about the project. """ module( name = "grpc-web", version = "1.6.0", compatibility_level = 1, repo_name = "com_github_grpc_grpc_web", ) bazel_dep(name = "protobuf", version = "27.1", repo_name = "com_google_protobuf") bazel_dep(name = "grpc", version = "1.65.0", repo_name = "com_github_grpc_grpc") bazel_dep(name = "rules_cc", version = "0.0.2") bazel_dep(name = "rules_proto", version = "6.0.2") # Needed to resolve https://github.com/bazelbuild/bazel-central-registry/issues/2538. single_version_override( module_name = "grpc-java", version = "1.64.0", ) ================================================ FILE: Makefile ================================================ ROOT_DIR := $(shell pwd) all: clean plugin: cd "$(ROOT_DIR)"/javascript/net/grpc/web/generator && make install-plugin: cd "$(ROOT_DIR)"/javascript/net/grpc/web/generator && make install clean: cd "$(ROOT_DIR)"/javascript/net/grpc/web/generator && make clean cd "$(ROOT_DIR)" ================================================ FILE: PATENTS ================================================ Additional IP Rights Grant (Patents) "This implementation" means the copyrightable works distributed by Google as part of the GRPC project. Google hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, transfer and otherwise run, modify and propagate the contents of this implementation of GRPC, where such license applies only to those patent claims, both currently owned or controlled by Google and acquired in the future, licensable by Google that are necessarily infringed by this implementation of GRPC. This grant does not include claims that would be infringed only as a consequence of further modification of this implementation. If you or your agent or exclusive licensee institute or order or agree to the institution of patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that this implementation of GRPC or any code incorporated within this implementation of GRPC constitutes direct or contributory patent infringement, or inducement of patent infringement, then any patent rights granted to you under this License for this implementation of GRPC shall terminate as of the date such litigation is filed. ================================================ FILE: README.md ================================================ # gRPC Web · [![npm version](https://img.shields.io/npm/v/grpc-web.svg?style=flat)](https://www.npmjs.com/package/grpc-web) A JavaScript implementation of [gRPC][] for browser clients. For more information, including a **quick start**, see the [gRPC-web documentation][grpc-web-docs]. gRPC-web clients connect to gRPC services via a special proxy; by default, gRPC-web uses [Envoy][]. In the future, we expect gRPC-web to be supported in language-specific web frameworks for languages such as Python, Java, and Node. For details, see the [roadmap](doc/roadmap.md). ## Streaming Support gRPC-web currently supports 2 RPC modes: - Unary RPCs ([example](#make-a-unary-rpc-call)) - Server-side Streaming RPCs ([example](#server-side-streaming)) (NOTE: Only when [`grpcwebtext`](#wire-format-mode) mode is used.) Client-side and Bi-directional streaming is not currently supported (see [streaming roadmap](doc/streaming-roadmap.md)). ## Quick Start Eager to get started? Try the [Hello World example][]. From this example, you'll learn how to do the following: - Define your service using protocol buffers - Implement a simple gRPC Service using Node.js - Configure the Envoy proxy - Generate protobuf message classes and client service stub for the client - Compile all the JS dependencies into a static library that can be consumed by the browser easily ## Advanced Demo: Browser Echo App You can also try to run a more advanced Echo app from the browser with a streaming example. From the repo root directory: ```sh $ docker-compose pull prereqs node-server envoy commonjs-client $ docker-compose up node-server envoy commonjs-client ``` Open a browser tab, and visit http://localhost:8081/echotest.html. To shutdown: `docker-compose down`. ## Runtime Library The gRPC-web runtime library is available on npm: ```sh $ npm i grpc-web ``` ## Code Generator Plugins ### (Prerequisite) 1. Protobuf (`protoc`) If you don't already have [`protoc`](https://github.com/protocolbuffers/protobuf) installed, download it first from [here](https://github.com/protocolbuffers/protobuf/releases) and install it on your PATH. If you use Homebrew (on macOS), you could run: ```sh brew install protobuf ``` ### (Prerequisite) 2. Protobuf-javascript (`protoc-gen-js`) If you don't have [`protoc-gen-js`](https://github.com/protocolbuffers/protobuf-javascript) installed, download it from [protocolbuffers/protobuf-javascript](https://github.com/protocolbuffers/protobuf-javascript/releases) and install it on your PATH. Or, use the [third-party](https://www.npmjs.com/package/protoc-gen-js) npm installer: ``` npm install -g protoc-gen-js ``` ### 3. Install gRPC-Web Code Generator You can download the `protoc-gen-grpc-web` protoc plugin from our [release](https://github.com/grpc/grpc-web/releases) page: Make sure all executables are discoverable from your PATH. For example, on macOS, you can do: ```sh sudo mv protoc-gen-grpc-web-2.0.2-darwin-aarch64 \ /usr/local/bin/protoc-gen-grpc-web chmod +x /usr/local/bin/protoc-gen-grpc-web ``` Note: If you are using our Docker setup, the `prereqs` image already includes both `protoc` and the `protoc-gen-grpc-web` plugin on the PATH. ### (Optional) 4. Verify Installations You can optionally verify the plugins work by following our [Hello World example](https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld#generating-stubs): ```sh cd net/grpc/gateway/examples/helloworld protoc -I=. helloworld.proto \ --js_out=import_style=commonjs:. \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:. ``` After the command runs successfully, you should now see two new files generated in the current directory. By running: ``` ls -1 *_pb.js ``` Installation is successful if you see the following 2 files: - `helloworld_pb.js` — Generated by `protoc-gen-js` plugin - `helloworld_grpc_web_pb.js` - Generated by gRPC-Web plugin ## Client Configuration Options Typically, you will run the following command to generate the proto messages and the service client stub from your `.proto` definitions: ```sh protoc -I=$DIR echo.proto \ --js_out=import_style=commonjs:$OUT_DIR \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:$OUT_DIR ``` You can then use Browserify, Webpack, Closure Compiler, etc. to resolve imports at compile time. ### Import Style `import_style=closure`: The default generated code has [Closure](https://developers.google.com/closure/library/) `goog.require()` import style. `import_style=commonjs`: The [CommonJS](https://requirejs.org/docs/commonjs.html) style `require()` is also supported. `import_style=commonjs+dts`: (Experimental) In addition to above, a `.d.ts` typings file will also be generated for the protobuf messages and service stub. `import_style=typescript`: (Experimental) The service stub will be generated in TypeScript. See **TypeScript Support** below for information on how to generate TypeScript files. > **Note:** The `commonjs+dts` and `typescript` styles are only supported by `--grpc-web_out=import_style=...`, not by `--js_out=import_style=...`. ### Wire Format Mode For more information about the gRPC-web wire format, see the [specification](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2). `mode=grpcwebtext`: The default generated code sends the payloads in the `grpc-web-text` format. - `Content-Type: application/grpc-web-text` - Payloads are base64-encoded. - Both unary and server streaming calls are supported. `mode=grpcweb`: A binary protobuf format is also supported. - `Content-Type: application/grpc-web+proto` - Payloads are in the binary protobuf format. - Only unary calls are supported. ## How It Works Let's take a look at how gRPC-web works with a simple example. You can find out how to build, run and explore the example yourself in [Build and Run the Echo Example](net/grpc/gateway/examples/echo). ### 1. Define your service The first step when creating any gRPC service is to define it. Like all gRPC services, gRPC-web uses [protocol buffers](https://developers.google.com/protocol-buffers) to define its RPC service methods and their message request and response types. ```protobuf message EchoRequest { string message = 1; } ... service EchoService { rpc Echo(EchoRequest) returns (EchoResponse); rpc ServerStreamingEcho(ServerStreamingEchoRequest) returns (stream ServerStreamingEchoResponse); } ``` ### 2. Run the server and proxy Next you need to have a gRPC server that implements the service interface and a gateway proxy that allows the client to connect to the server. Our example builds a simple Node gRPC backend server and the Envoy proxy. For the Echo service: see the [service implementation](net/grpc/gateway/examples/echo/node-server/server.js). For the Envoy proxy: see the [config YAML file](net/grpc/gateway/examples/echo/envoy.yaml). ### 3. Write your JS client Once the server and gateway are up and running, you can start making gRPC calls from the browser! Create your client: ```js var echoService = new proto.mypackage.EchoServiceClient( 'http://localhost:8080'); ``` #### Make a unary RPC call: ```js var request = new proto.mypackage.EchoRequest(); request.setMessage(msg); var metadata = {'custom-header-1': 'value1'}; echoService.echo(request, metadata, function(err, response) { if (err) { console.log(err.code); console.log(err.message); } else { console.log(response.getMessage()); } }); ``` #### Server-side streaming: ```js var stream = echoService.serverStreamingEcho(streamRequest, metadata); stream.on('data', function(response) { console.log(response.getMessage()); }); stream.on('status', function(status) { console.log(status.code); console.log(status.details); console.log(status.metadata); }); stream.on('end', function(end) { // stream end signal }); // to close the stream stream.cancel(); ``` For an in-depth tutorial, see [this page](net/grpc/gateway/examples/echo/tutorial.md). ## Setting a Deadline You can set a deadline for your RPC by setting a `deadline` header. The value should be a Unix timestamp, in milliseconds. ```js var deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 1); client.sayHelloAfterDelay(request, {deadline: deadline.getTime().toString()}, (err, response) => { // err will be populated if the RPC exceeds the deadline ... }); ``` ## TypeScript Support The `grpc-web` module can now be imported as a TypeScript module. This is currently an experimental feature. Any feedback welcome! When using the `protoc-gen-grpc-web` protoc plugin, mentioned above, pass in either: - `import_style=commonjs+dts`: existing CommonJS style stub + `.d.ts` typings - `import_style=typescript`: full TypeScript output Do *not* use `import_style=typescript` for `--js_out`, it will silently be ignored. Instead you should use `--js_out=import_style=commonjs`, or `--js_out=import_style=commonjs,binary` if you are using `mode=grpcweb`. The `--js_out` plugin will generate JavaScript code (`echo_pb.js`), and the `-grpc-web_out` plugin will generate a TypeScript definition file for it (`echo_pb.d.ts`). This is a temporary hack until the `--js_out` supports TypeScript itself. For example, this is the command you should use to generate TypeScript code using the binary wire format: ```sh protoc -I=$DIR echo.proto \ --js_out=import_style=commonjs,binary:$OUT_DIR \ --grpc-web_out=import_style=typescript,mode=grpcweb:$OUT_DIR ``` It will generate the following files: * `EchoServiceClientPb.ts` - Generated by `--grpc-web_out`, contains the TypeScript gRPC-web code. * `echo_pb.js` - Generated by `--js_out`, contains the JavaScript Protobuf code. * `echo_pb.d.ts` - Generated by `--grpc-web_out`, contains TypeScript definitions for `echo_pb.js`. ### Using Callbacks ```ts import * as grpcWeb from 'grpc-web'; import {EchoServiceClient} from './EchoServiceClientPb'; import {EchoRequest, EchoResponse} from './echo_pb'; const echoService = new EchoServiceClient('http://localhost:8080', null, null); const request = new EchoRequest(); request.setMessage('Hello World!'); const call = echoService.echo(request, {'custom-header-1': 'value1'}, (err: grpcWeb.RpcError, response: EchoResponse) => { console.log(response.getMessage()); }); call.on('status', (status: grpcWeb.Status) => { // ... }); ``` (See [here](https://github.com/grpc/grpc-web/blob/4d7dc44c2df522376394d3e3315b7ab0e010b0c5/packages/grpc-web/index.d.ts#L29-L39) for the full list of possible `.on(...)` callbacks.) ### (Optional) Using Promises (Limited Features) > **NOTE:** It is not possible to access the `.on(...)` callbacks (e.g. for `metadata` and `status`) when Promise is used. ```ts // Create a Promise client instead const echoService = new EchoServicePromiseClient('http://localhost:8080', null, null); ... (same as above) this.echoService.echo(request, {'custom-header-1': 'value1'}) .then((response: EchoResponse) => { console.log(`Received response: ${response.getMessage()}`); }).catch((err: grpcWeb.RpcError) => { console.log(`Received error: ${err.code}, ${err.message}`); }); ``` For the full TypeScript example, see [ts-example/client.ts](net/grpc/gateway/examples/echo/ts-example/client.ts) and the [instructions](net/grpc/gateway/examples/echo/ts-example) to run. ## Custom Interceptors Custom interceptors can be implemented and chained, which could be useful for features like auth, retries, etc. There are 2 types of interceptors ([interfaces](https://github.com/grpc/grpc-web/blob/3cd7e0d43493d4694fed78400e4ad78031d70c09/packages/grpc-web/index.d.ts#L55-L65)): - `UnaryInterceptor` ([doc](https://grpc.io/blog/grpc-web-interceptor/#stream-interceptor-example), [example](https://github.com/grpc/grpc-web/blob/master/packages/grpc-web/test/tsc-tests/client04.ts)) - Intercept Unary RPCs; can only be used with Promise clients. - `StreamInterceptor` ([doc](https://grpc.io/blog/grpc-web-interceptor/#stream-interceptor-example), [example](https://github.com/grpc/grpc-web/blob/master/packages/grpc-web/test/tsc-tests/client03.ts)) - More versatile; can be used with regular clients. For more details, see [this blog post](https://grpc.io/blog/grpc-web-interceptor/). ## Ecosystem ### Proxy Interoperability Multiple proxies support the gRPC-web protocol. 1. The current **default proxy** is [Envoy][], which supports gRPC-web out of the box. ```sh $ docker-compose up -d node-server envoy commonjs-client ``` 2. You can also try the [gRPC-web Go Proxy][]. ```sh $ docker-compose up -d node-server grpcwebproxy binary-client ``` 3. Apache [APISIX](https://apisix.apache.org/) has also added gRPC-web support, and more details can be found [here](https://apisix.apache.org/blog/2022/01/25/apisix-grpc-web-integration/). 4. [Nginx](https://www.nginx.com/) has a gRPC-web module ([doc](https://nginx.org/en/docs/http/ngx_http_grpc_module.html), [announcement](https://www.nginx.com/blog/nginx-1-13-10-grpc/))), and seems to work with simple configs, according to user [feedback](https://github.com/grpc/grpc-web/discussions/1322). ### Server Frameworks with gRPC-Web support - [Armeria (JVM)](https://armeria.dev/docs/server-grpc/#grpc-web) - [Tonic (Rust)](https://docs.rs/tonic-web/latest/tonic_web/) ### Web Frameworks Compatibility - **Vite** - See this [demo app](https://github.com/a2not/vite-grpc-web), as well as this [comment](https://github.com/grpc/grpc-web/issues/1242#issuecomment-1816249928). [Envoy]: https://www.envoyproxy.io [gRPC]: https://grpc.io [grpc-web-docs]: https://grpc.io/docs/languages/web [gRPC-web Go Proxy]: https://github.com/improbable-eng/grpc-web/tree/master/go/grpcwebproxy [Hello World example]: net/grpc/gateway/examples/helloworld ================================================ FILE: SECURITY.md ================================================ # Security Policy For information on gRPC Security Policy and reporting potentional security issues, please see [gRPC CVE Process](https://github.com/grpc/proposal/blob/master/P4-grpc-cve-process.md). ================================================ FILE: doc/browser-features.md ================================================ # gRPC-Web features for browser (HTML) clients Due to browser limitation, gRPC-Web uses a different transport than the [HTTP/2 based gRPC protocol](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md). The difference between the gRPC-Web protocol and the HTTP/2 based gRPC protocol is specified in the core gRPC repo as [PROTOCOL-WEB](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md). In addition to the wire-transport spec, gRPC-Web also supports features that are unique to browser (HTML) clients. This document serves as the official spec for those features. As the Web platform evolves, we expect some of those features will evolve too or become deprecated. On the server-side, [Envoy](https://www.envoyproxy.io/) is the official proxy with built-in gRPC-Web support. New features will be implemented in Envoy first. For [in-process gRPC-Web support](https://github.com/grpc/grpc-web/blob/master/doc/in-process-proxy.md), we recommend that the gRPC-Web module implement only a minimum set of features, e.g. to enable local development. Those features are identified as mandatory features in this doc. # CORS support * Should follow the [CORS spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Server-Side_Access_Control) (Mandatory) * Access-Control-Allow-Credentials to allow Authorization headers * Access-Control-Allow-Methods to allow POST and (preflight) OPTIONS only * Access-Control-Allow-Headers to whatever the preflight request carries * Access-Control-Expose-Headers to allow Javascript access to `grpc-status,grpc-message` headers. * The client library is expected to support header overwrites to avoid preflight * https://github.com/whatwg/fetch/issues/210 * CSP support to be specified # HTTP status code mapping A grpc-web gateway is recommended to overwrite the default 200 status code and map any gateway-generated or server-generated error status to standard HTTP status codes (such as 503) when it is possible. This will help with debugging and may also improve security protection for web apps. # URL query params To enable query-param based routing rules in reverse proxies and to avoid CORS preflight, a grpc-web client may "advertise" certain request data or metadata as query params. The grpc-web proxy module should remove the query params before the request is sent to the downstream gRPC server. The actual data in query params is not interpreted by grpc-web libraries. Standard URL encoding rules shoud be followed to encode those query params. # Security * XSRF, XSS policy to be published # Compression * Full-body compression is supported and expected for all unary requests/responses. The compression/decompression will be done by browsers, using standard Content-Encoding headers * “grpc-encoding” header is not used * SDCH, Brotli will be supported * Message-level compression for streamed requests/responses is not supported because manual compression/decompression is prohibitively expensive using JS ================================================ FILE: doc/in-process-proxy.md ================================================ # Overview In-process proxies allow a browser client to talk to a gRPC server directly without relying on any intermediary process such as an Envoy proxy. This document provides a high-level design guidelines on how we expect such a "proxy" to work. # The choice of HTTP stack We strongly recommend that the gRPC-Web module use the default HTTP stack provided by the language platform, or in the case of Java, the standard Java Servlet framework. This is to ensure maximum portability and to ease integration between gRPC-Web and existing Web frameworks. The actual HTTP version that the HTTP stack supports may include both HTTP/1.1 and HTTP/2. In the runtime, it's up to the user-agent and intermediaries to negotiate the HTTP version, which is transparent to the gRPC-Web module. # Request translation For most languages, the gRPC-Web module will handle the gRPC-Web request, perform the translation, and then proxy the request using a gRPC client to the gRPC server via a local socket. The gRPC-Web support is fully transparent to the gRPC server. For some languages, such as Swift, .NET, if the gRPC server implementation uses the same HTTP stack that the gRPC-Web module uses, then gRPC-Web may be supported directly as part of the gRPC server implementation. The added complexity to the gRPC implementation itself is still a concern. # HTTP port We expect that gRPC-Web requests are handled on a separate port. If the HTTP stack supports both HTTP/2 and HTTP/1.1, port sharing could be supported. However, since CORS is a mandatory feature for gRPC-Web proxies, port sharing should be optional for in-process proxies. # Core features The gRPC-Web module should implement only the [core gRPC-Web features](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) and leave to the HTTP/Web stack provided by the language platform to handle [Web-framework-level features](https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md) such as XSRF, CORS policies. Some of those features may be incompatible with what Envoy supports for gRPC-Web. ================================================ FILE: doc/interop-test-descriptions.md ================================================ gRPC-Web Interop Tests ====================== This document describes the set of tests any gRPC-Web clients or proxies need to implement. The proto definition for the messages and RPCs we are using for the tests can be found [here](https://github.com/grpc/grpc/blob/master/src/proto/grpc/testing/test.proto). The canonical set of interop tests was defined in the main [grpc/grpc repo](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). Here in gRPC-Web, we will only implement a subset of tests that's relevant to gRPC-Web. For example, we will not be implementing any tests involving client-streaming or bidi-streaming for now. On the other hand, there are gRPC-Web specific tests that we will add here. ``` gRPC-Web Client <--> Proxy <--> gRPC Service ``` The idea is that we should be able to swap out any of the 3 components above and all the interop tests should still pass. This repository will provide a canonical implementation of the interop test suite using the Javascript client, Envoy and a gRPC service implemented in Node. For any new gRPC-Web client implementation, you need to swap out the JS client and make sure all tests still pass. For any in-process proxies implementation, you need to swap out the proxy and the service as a unit and make sure the standard JS client will still pass the tests. List of Tests ------------- | Test Name | grpc-web-text Mode | grpc-web Binary mode | | --------- |:------------------:|:--------------------:| | empty_unary | ✓ | ✓ | | cacheable_unary | TBD | TBD | | large_unary | ✓ | ✓ | | client_compressed_unary | ✓ | ✓ | | server_compressed_unary | ✓ | ✓ | | client_streaming | ✗ | ✗ | | client_compressed_streaming | ✗ | ✗ | | server_streaming | ✓ | ✗ | | server_compressed_streaming | ✓ | ✗ | | ping_pong | ✗ | ✗ | | empty_stream | ✗ | ✗ | | compute_engine_creds | TBD | TBD | | jwt_token_creds | TBD | TBD | | oauth2_auth_token | TBD | TBD | | per_rpc_creds | TBD | TBD | | google_default_credentials | TBD | TBD | | compute_engine_channel_credentials | TBD | TBD | | custom_metadata * | ✓ | ✓ | | status_code_and_message * | ✓ | ✓ | | special_status_message | ✓ | ✓ | | unimplemented_method | ✓ | ✓ | | unimplemented_service | ✓ | ✓ | | cancel_after_begin | ✗ | ✗ | | cancel_after_first_response | ✗ | ✗ | | timeout_on_sleeping_server | ✗ | ✗ | \* only need to implement the UnaryCall RPC gRPC-Web specific considerations -------------------------------- ### Text vs Binary mode As mentioned in the table above, client needs to be tested in both the text format `application/grpc-web-text` and the binary mode `application/grpc-web+proto`. The latter we don't need to test any streaming methods. ### CORS and other web specific scenarios We may add specific tests to account for web-related scenarios like CORS handling, etc. Mostly these are to test the connection between the browser client and the proxy. ================================================ FILE: doc/roadmap.md ================================================ # gRPC-Web Roadmap The purpose of this document is to collect all the features that we believe are useful for gRPC users. ## Background gRPC-Web has been developed internally at Google as part of the front-end stacks for Google's Web applications and cloud services. Over time we plan to open-source and publish most of the features and make them available to open-source users. Like everywhere, Web platforms and technologies are constantly evolving, often with many inter-dependent ecosystems. As much as we like to open-source everything, we also need keep the balance between creating a reusable and stable open-source solution and meeting those requirements unique to Google's Web applications (such as search). ## Roadmap Features > NOTE: Due to the status of two of gRPC-Web’s core dependencies — [Google Closure](https://github.com/google/closure-library/issues/1214), which has been archived, and [Protobuf JavaScript](https://github.com/protocolbuffers/protobuf-javascript?tab=readme-ov-file#project-status), which is receiving only minimal updates — the gRPC-Web project is no longer able to deliver new, modern solutions for the open source community. As a result, we do not plan to be adding new features going forward. > > We recommend you to use [gRPC-Gateway](https://github.com/grpc-ecosystem/grpc-gateway) as an alternative. ### TypeScript Codebase Migrate the codebase to TypeScript and update the related toolchains (incl. remove dependency on `closure-compiler`). Enhance overall TypeScript support. ### Streaming Support Enhance Fetch/streams support (e.g. cancellation support) and improve runtime support, including service workers. See streaming roadmap [here](streaming-roadmap.md). ### Non-Binary Message Encoding The binary protobuf encoding format is not most CPU efficient for browser clients. Furthermore, the generated code size increases as the total protobuf definition increases. For Google's Web applications (e.g. gmail), we use a JSON like format which is comparable to JSON in efficiency but also very compact in both the message size and code size. ### Security We plan to publish a comprehensive guideline doc on how to create secure Web applications. Native support such as XSRF, XSS prevention may also be added to the gRPC-Web protocol. ### Web Framework Integration This is to provide first-class support for gRPC API and gRPC-Web in popular Web frameworks such as Angular. Note: Dart gRPC will use gRPC-Web as the underlying implementation on the Dart Web platform. ### Non-Closure compiler support With the addition of CommonJS style imports, gRPC-Web client stubs can now be compiled with various tools such as Browserify, Webpack, etc. Let us know what else we should try! ================================================ FILE: doc/streaming-roadmap.md ================================================ # Streaming Roadmap This document describes the road-map for gRPC-Web to support different streaming features. * Server-streaming * Client-streaming and half-duplex streaming ## Server-streaming We will keep improving server-streaming in the following areas: * Fetch cancellation support - 2024 * Performance improvements and whatwg Fetch/streams support, including service workers - 2024 * Finalizing keep-alive support (via Envoy) - 2024+ * Addressing runtime behavior gaps between Fetch and XHR - 2024+ ## Client-streaming and half-duplex streaming We don’t plan to support client-streaming via Fetch/upload-streams (See [Appendix](#chrome-origin-trial-on-upload-streaming) on backgrounds on the Chrome Origin Trial). As a result, half-duplex bidi streaming won’t be supported via Fetch/streams either. Client-streaming and half-duplex bidi streaming will be addressed when Full-duplex streaming is supported via WebTransport (see below). ## Full-duplex streaming Not planned. ## Issues with WebSockets We have no plan to support full-duplex streaming over WebSockets (over TCP or HTTP/2). We will not publish any experimental spec for gRPC over WebSockets either. The main issue with WebSockets is its incompatibility with HTTP, i.e. the ubiquitous Web infrastructure. This means HTTP fallback is always needed. Recent IETF proposal to tunnel WebSockets over HTTP/2 is not widely implemented either. ## Appendix ### Chrome Origin Trial on `upload-streaming` We worked on a Chrome [Origin Trial](https://developers.chrome.com/origintrials/#/view_trial/3524066708417413121) to finalize the fetch/upload stream API spec (whatwg). One of the pending issues that blocks the final spec is to decide whether it is safe to enable upload-streaming over HTTP/1.1. We believe that upload-streaming should be enabled for both HTTP/2 and HTTP/1.1. Specifically for gRPC-Web, the server can't control the client deployment. As a result, if upload-streaming is only enabled over HTTP/2, a gRPC service will have to implement a non-streaming method as a fallback for each client-streaming method. ================================================ FILE: docker-compose.yml ================================================ version: '3' services: prereqs: build: context: ./ dockerfile: ./net/grpc/gateway/docker/prereqs/Dockerfile image: grpcweb/prereqs echo-server: build: context: ./ dockerfile: ./net/grpc/gateway/docker/echo_server/Dockerfile depends_on: - prereqs image: grpcweb/echo-server ports: - "9090:9090" node-server: build: context: ./ dockerfile: ./net/grpc/gateway/docker/node_server/Dockerfile depends_on: - prereqs image: grpcweb/node-server ports: - "9090:9090" node-interop-server: build: context: ./ dockerfile: ./net/grpc/gateway/docker/node_interop_server/Dockerfile image: grpcweb/node-interop-server ports: - "7074:7074" envoy: build: context: ./ dockerfile: ./net/grpc/gateway/docker/envoy/Dockerfile image: grpcweb/envoy ports: - "8080:8080" links: - node-server grpcwebproxy: build: context: ./ dockerfile: ./net/grpc/gateway/docker/grpcwebproxy/Dockerfile image: grpcweb/grpcwebproxy ports: - "8080:8080" links: - node-server commonjs-client: build: context: ./ dockerfile: ./net/grpc/gateway/docker/commonjs_client/Dockerfile depends_on: - prereqs image: grpcweb/commonjs-client ports: - "8081:8081" closure-client: build: context: ./ dockerfile: ./net/grpc/gateway/docker/closure_client/Dockerfile depends_on: - prereqs image: grpcweb/closure-client ports: - "8081:8081" ts-client: build: context: ./ dockerfile: ./net/grpc/gateway/docker/ts_client/Dockerfile depends_on: - prereqs image: grpcweb/ts-client ports: - "8081:8081" binary-client: build: context: ./ dockerfile: ./net/grpc/gateway/docker/binary_client/Dockerfile depends_on: - prereqs image: grpcweb/binary-client ports: - "8081:8081" interop-client: build: context: ./ dockerfile: ./net/grpc/gateway/docker/interop_client/Dockerfile depends_on: - prereqs image: grpcweb/interop-client ports: - "8081:8081" jsunit-test: build: context: ./ dockerfile: ./packages/grpc-web/docker/jsunit-test/Dockerfile image: grpcweb/jsunit-test ================================================ FILE: etc/localhost.crt ================================================ -----BEGIN CERTIFICATE----- MIIC9jCCAd4CCQCfXxHXagE8mjANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJV UzELMAkGA1UECAwCQ0ExITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 ZDAeFw0xODA4MDMyMTE2NDdaFw0yMTA1MjMyMTE2NDdaMD0xCzAJBgNVBAYTAlVT MQswCQYDVQQIDAJDQTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2/MlKj+OtIgJm/7DywOR POypfvGHXqHTpg/ZbZgflx2vMwgoAhdun2e//AlssouStadnkevPEr+uFfxkEzH3 80iYDtcZKXY8E6692hFrp7hKnA7gcBbb7ZQ1FwG/SfKLtLcderLcQb51P7IsQkfh nB8hSosV9nHhdfVtsMW7L/caqB5lUHIbRsHhSw3+hzg0r0HuKxXd5HlyRXzf9cQX 4xc5B8Ldxo3QmXDOUHDw9quuxvUn5VWppWCGn2J+f9L/5iwgciApbiMBv/CkiVrt iYwZY+TZY5u8lmL4FtLd2tj2vNXl5ESWcL1SRGSiaYmxX1B5rg4fSAAXmcNOzZHo 8wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCHko8eFag++9knWV8KlRRi+IdGeatU TejdBVnCPFc7sJf1lkUQSb0mZMv0QEO51aXGVvU46pIjTAwtzcVgPc6ZHqcZY4r3 xscrmECThbhsEQCHqDD55OB2a06bx+ylfbBnLh+F18W+/rI+HlRxSBGclyfVto1P aCuYvYc0qKK90Ft1joZh1tXpho/D52B4CTa0Ax/5UqSVjTt0uPDhkCZJKnoENVgh 6hF8ehYTC6Kf6ZtbB6+GuaLXf6F96CROLifW219qxrKmGbMyJXolOxLatufnWwwv Hw7z1FUzulJUkSRmgPJ9hFeyTjCS1BJ18glFjleLykYOtQi8kvnpFm6C -----END CERTIFICATE----- ================================================ FILE: etc/localhost.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEA2/MlKj+OtIgJm/7DywORPOypfvGHXqHTpg/ZbZgflx2vMwgo Ahdun2e//AlssouStadnkevPEr+uFfxkEzH380iYDtcZKXY8E6692hFrp7hKnA7g cBbb7ZQ1FwG/SfKLtLcderLcQb51P7IsQkfhnB8hSosV9nHhdfVtsMW7L/caqB5l UHIbRsHhSw3+hzg0r0HuKxXd5HlyRXzf9cQX4xc5B8Ldxo3QmXDOUHDw9quuxvUn 5VWppWCGn2J+f9L/5iwgciApbiMBv/CkiVrtiYwZY+TZY5u8lmL4FtLd2tj2vNXl 5ESWcL1SRGSiaYmxX1B5rg4fSAAXmcNOzZHo8wIDAQABAoIBAFPnJL5BEIb9fezr +nRvH/BFt0KdkC4hPUOTuDV+Wk6jHDozWk+x8JkOUsYqMjTJ2WVCPtgDRDK6vAXX CbXo0dUUVC0VEJwoZjJ77iBJlO+d9ZgidKtNjQfMCZSFLhtfUrvVPoGXyT2rEb8C kK+YDBAqL+DnvbENMBx3SyirxQQ+YetAUSxiZagtjKlax1bhXF/JCj816ezDqOzR ZVx4MJiJg3oD+zKlwP+IaFlANIuW2W7+LbNzPpdXER4gafRzyjRy5ksO9NFO11Bu 2srJbAMZEr0MCEBjf5rD7CPuvhTTEcgk6CNPsIEt8zzMSZUMeS0xaIv0W1FKiw+R BENntsECgYEA84fSMRApKaweeBrthPKR1ucnv3EHxn1l1mz45bp+2euHB6jUul9D 629mMv8J9PVdcPF5ck7YpCjslWm6oOhtKMemuwJm61cRF44Lz7jtLm3zNJMGhpbh 6Q0GoIVcyMrW79BODbEIU5SlWp0Usyjo/4CZEP5adho1JNTDHCyvEVUCgYEA5zY6 tbfu+YYgICBnaRkGgdCRBxMurxdPrgbwvrKSMerYE9fufpgvZc5wwx02rJX0psuo JNGLAkPJyimZCYhY/hxWwXQX8X4BhkKK2aFyBMaDIBA0h+unOwTxHrebQNprot3h YVT8+tfZf8bGPl+dBs3Qf+WOESjRSyO9jr5BMScCgYBfI4mPF1Qtbot8unBeRvGI tkeF999kwOp/CZV3EhOqiOP4rxFkOgFrwdp4Q8CdDRpTHFMov/rMrxw2BtcdM5Ap pU3Ss06H1DzeKeUdYo5uXA/uUx3yiJF7HVagcVldLDkp+QP1P1sUY/bxXnqOv4W/ A3tI80Vd7EEkwWXz5NUD/QKBgGXhYXFdQTI2RcWiQa7v1gwxqRYi/7krXnLioAaH jR/tyZTE21RxHsGPe+Sd5M+brBgrOUYwBz7SPAKW3dZzfDNMrXXFAB/rVCSjAafw Gdu81V61hVA3KJM7FDxiz0h+dltnxb4rwuWNY0uIfSZS31B2NF+G+VjaUY74irhx YSyVAoGANrn70s5+cJDWcmDhaFNU/J/X2Q8GgTyBRd02FIvuv7BXgd9TZ7bUVTws 1nsQCCEVJqj4ksddVRq33NvsnjrgetGYe1LGl3uLakqaXd7iJXFCice3ZuFFrp9o Iq2sUgG+9K8WFqNhANRKBVd32IpQzjIMAAJSbuG3EFZDLZJqxDs= -----END RSA PRIVATE KEY----- ================================================ FILE: javascript/net/grpc/web/abstractclientbase.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * @fileoverview gRPC browser client library. * * Base interface for gRPC Web JS clients * * @author stanleycheung@google.com (Stanley Cheung) */ goog.module('grpc.web.AbstractClientBase'); goog.module.declareLegacyNamespace(); const ClientReadableStream = goog.require('grpc.web.ClientReadableStream'); const MethodDescriptor = goog.require('grpc.web.MethodDescriptor'); const RpcError = goog.require('grpc.web.RpcError'); /** * @constructor * @struct * @final */ const PromiseCallOptions = function() {}; /** * An AbortSignal to abort the call. * @type {AbortSignal|undefined} */ PromiseCallOptions.prototype.signal; /** * This interface represents a grpc-web client * @interface */ const AbstractClientBase = class { constructor() {} /** * @abstract * @template REQUEST, RESPONSE * @param {string} method The method to invoke * @param {REQUEST} requestMessage The request proto * @param {!Object} metadata User defined call metadata * @param {!MethodDescriptor} * methodDescriptor Information of this RPC method * @param {function(?RpcError, ?)} * callback A callback function which takes (error, RESPONSE or null) * @return {!ClientReadableStream} */ rpcCall(method, requestMessage, metadata, methodDescriptor, callback) {} /** * @abstract * @protected * @template REQUEST, RESPONSE * @param {string} method The method to invoke * @param {REQUEST} requestMessage The request proto * @param {!Object} metadata User defined call metadata * @param {!MethodDescriptor} * methodDescriptor Information of this RPC method * @param options Options for the call * @return {!IThenable} * A promise that resolves to the response message */ thenableCall(method, requestMessage, metadata, methodDescriptor, options) {} /** * @abstract * @template REQUEST, RESPONSE * @param {string} method The method to invoke * @param {REQUEST} requestMessage The request proto * @param {!Object} metadata User defined call metadata * @param {!MethodDescriptor} * methodDescriptor Information of this RPC method * @return {!ClientReadableStream} The Client Readable Stream */ serverStreaming(method, requestMessage, metadata, methodDescriptor) {} }; /** * Get the hostname of the current request. * @template REQUEST, RESPONSE * @param {string} method * @param {!MethodDescriptor} methodDescriptor * @return {string} */ function getHostname(method, methodDescriptor) { // method = hostname + methodDescriptor.name(relative path of this method) return method.substr(0, method.length - methodDescriptor.name.length); } exports = {AbstractClientBase, PromiseCallOptions, getHostname}; ================================================ FILE: javascript/net/grpc/web/calloptions.js ================================================ /** * @fileoverview grpc.web.CallOptions */ goog.module('grpc.web.CallOptions'); goog.module.declareLegacyNamespace(); /** * The collection of runtime options for a new RPC call. * @unrestricted */ class CallOptions { /** * @param {!Object=} options */ constructor(options) { /** * @const {!Object} * @private */ this.properties_ = options || {}; } /** * Add a new CallOption or override an existing one. * * @param {string} name name of the CallOption that should be * added/overridden. * @param {VALUE} value value of the CallOption * @template VALUE */ setOption(name, value) { this.properties_[name] = value; } /** * Get the value of one CallOption. * * @param {string} name name of the CallOption. * @return {!Object} value of the CallOption. If name doesn't exist, will * return 'undefined'. */ get(name) { return this.properties_[name]; } /** * Remove a CallOption. * * @param {string} name name of the CallOption that shoud be removed. */ removeOption(name) { delete this.properties_[name]; } /** * @return {!Array} */ getKeys() { return Object.keys(this.properties_); } } exports = CallOptions; ================================================ FILE: javascript/net/grpc/web/clientoptions.js ================================================ goog.module('grpc.web.ClientOptions'); goog.module.declareLegacyNamespace(); const {StreamInterceptor, UnaryInterceptor} = goog.require('grpc.web.Interceptor'); /** * Options that are available during the client construction. * @record */ class ClientOptions { constructor() { /** * Whether to use the HttpCors library to pack http headers into a special * url query param $httpHeaders= so that browsers can bypass CORS OPTIONS * requests. * @type {boolean|undefined} */ this.suppressCorsPreflight; /** * Whether to turn on XMLHttpRequest's withCredentials flag. * @type {boolean|undefined} */ this.withCredentials; /** * Unary interceptors. Note that they are only available in grpcweb and * grpcwebtext mode * @type {!Array|undefined} */ this.unaryInterceptors; /** * Stream interceptors. Note that they are only available in grpcweb and * grpcwebtext mode * @type {!Array|undefined} */ this.streamInterceptors; /** * Protocol buffer format for open source gRPC-Web. This attribute should be * specified by the gRPC-Web build rule by default. * @type {string|undefined} */ this.format; /** * The Worker global scope. Once this option is specified, gRPC-Web will * also use 'fetch' API as the underlying transport instead of native * XmlHttpRequest. * @type {!WorkerGlobalScope|undefined} */ this.workerScope; /** * This is an experimental feature to reduce memory consumption * during high throughput server-streaming calls by using * 'streamBinaryChunks' mode FetchXmlHttpFactory. * @type {boolean|undefined} */ this.useFetchDownloadStreams; } } exports = ClientOptions; ================================================ FILE: javascript/net/grpc/web/clientreadablestream.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * @fileoverview gRPC web client Readable Stream * * This class is being returned after a gRPC streaming call has been * started. This class provides functionality for user to operates on * the stream, e.g. set onData callback, etc. * * This wraps the underlying goog.net.streams.NodeReadableStream * * @author stanleycheung@google.com (Stanley Cheung) */ goog.module('grpc.web.ClientReadableStream'); goog.module.declareLegacyNamespace(); /** * A stream that the client can read from. Used for calls that are streaming * from the server side. * * @template RESPONSE * @interface */ const ClientReadableStream = function() {}; /** * Register a callback to handle different stream events. * * Available event types for gRPC-Web: * 'data': The 'data' event is emitted when a new response message chunk is * received and successfully handled by gRPC-Web client. * 'status': the google RPC status of the response stream. * 'end': The 'end' event is emitted when all the data have been successfully * consumed from the stream. * 'error': typically, this may occur when an underlying internal failure * happens, or a stream implementation attempts to push an invalid chunk of * data. * 'metadata': the response metadata. Response headers should be read via * 'metadata' callbacks. * * For server-streaming calls. the 'data' and 'status' callbacks (if exist) * will always precede 'metadata', 'error', or 'end' callbacks. * * @param {string} eventType The event type * @param {function(?)} callback The callback to handle the event with * an optional input object * @return {!ClientReadableStream} this object */ ClientReadableStream.prototype.on = goog.abstractMethod; /** * Remove a particular callback. * * @param {string} eventType The event type * @param {function(?)} callback The callback to remove * @return {!ClientReadableStream} this object */ ClientReadableStream.prototype.removeListener = goog.abstractMethod; /** * Close the stream. */ ClientReadableStream.prototype.cancel = goog.abstractMethod; exports = ClientReadableStream; ================================================ FILE: javascript/net/grpc/web/clientunarycallimpl.js ================================================ /** * @fileoverview This class handles ClientReadableStream returned by unary * calls. */ goog.module('grpc.web.ClientUnaryCallImpl'); goog.module.declareLegacyNamespace(); const ClientReadableStream = goog.require('grpc.web.ClientReadableStream'); /** * @implements {ClientReadableStream} * @template RESPONSE */ class ClientUnaryCallImpl { /** * @param {!ClientReadableStream} stream */ constructor(stream) { this.stream = stream; } /** * @override */ on(eventType, callback) { if (eventType == 'data' || eventType == 'error') { // unary call responses and errors should be handled by the main // (err, resp) => ... callback return this; } return this.stream.on(eventType, callback); } /** * @override */ removeListener(eventType, callback) { return this.stream.removeListener(eventType, callback); } /** * @override */ cancel() { this.stream.cancel(); } } exports = ClientUnaryCallImpl; ================================================ FILE: javascript/net/grpc/web/generator/BUILD.bazel ================================================ load("@rules_cc//cc:defs.bzl", "cc_binary") cc_binary( name = "protoc-gen-grpc-web", srcs = [ "grpc_generator.cc", ], visibility = ["//visibility:public"], deps = [ "@com_google_protobuf//:protoc_lib", ], ) ================================================ FILE: javascript/net/grpc/web/generator/Makefile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. CXX ?= g++ CPPFLAGS += -I/usr/local/include -pthread CXXFLAGS += -std=c++11 LDFLAGS += -L/usr/local/lib -lprotoc -lprotobuf -lpthread -ldl PREFIX ?= /usr/local MIN_MACOS_VERSION := 10.7 # Supports OS X Lion STATIC ?= yes UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) CXXFLAGS += -stdlib=libc++ -mmacosx-version-min=$(MIN_MACOS_VERSION) else ifeq ($(UNAME_S),Linux) ifeq ($(STATIC),yes) LDFLAGS += -static endif endif all: protoc-gen-grpc-web protoc-gen-grpc-web: grpc_generator.o $(CXX) $^ $(LDFLAGS) -o $@ install: protoc-gen-grpc-web mkdir -p $(PREFIX)/bin install protoc-gen-grpc-web $(PREFIX)/bin/protoc-gen-grpc-web clean: rm -f *.o protoc-gen-grpc-web ================================================ FILE: javascript/net/grpc/web/generator/grpc_generator.cc ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #include #include #include #include #include #include #include #include #include #include #include using google::protobuf::Descriptor; using google::protobuf::Edition; using google::protobuf::EnumDescriptor; using google::protobuf::FieldDescriptor; using google::protobuf::FileDescriptor; using google::protobuf::MethodDescriptor; using google::protobuf::ServiceDescriptor; using google::protobuf::FieldOptions; using google::protobuf::OneofDescriptor; using google::protobuf::compiler::CodeGenerator; using google::protobuf::compiler::GeneratorContext; using google::protobuf::compiler::ParseGeneratorParameter; using google::protobuf::compiler::PluginMain; using google::protobuf::compiler::Version; using google::protobuf::io::Printer; using google::protobuf::io::ZeroCopyOutputStream; namespace grpc { namespace web { namespace { using std::string; enum Mode { OP = 0, // first party google3 one platform services GRPCWEB = 1, // client using the application/grpc-web wire format }; enum ImportStyle { CLOSURE = 0, // goog.require("grpc.web.*") COMMONJS = 1, // const grpcWeb = require("grpc-web") TYPESCRIPT = 2, // import * as grpcWeb from 'grpc-web' }; const char GRPC_PROMISE[] = "grpc.web.promise.GrpcWebPromise"; const char* kKeyword[] = { "abstract", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "debugger", "default", "delete", "do", "double", "else", "enum", "export", "extends", "false", "final", "finally", "float", "for", "function", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "typeof", "var", "void", "volatile", "while", "with", }; // Edit the version here prior to release static const std::string GRPC_WEB_VERSION = "2.0.2"; string GetProtocVersion(GeneratorContext* context) { Version compiler_version; context->GetCompilerVersion(&compiler_version); return std::to_string(compiler_version.major()) + "." + std::to_string(compiler_version.minor()) + "." + std::to_string(compiler_version.patch()) + compiler_version.suffix(); } bool IsReserved(const string& ident) { for (size_t i = 0; i < sizeof(kKeyword) / sizeof(kKeyword[0]); i++) { if (ident == kKeyword[i]) { return true; } } return false; } string GetModeVar(const Mode mode) { switch (mode) { case OP: return "OP"; case GRPCWEB: return "GrpcWeb"; } return ""; } string GetDeserializeMethodName(std::map vars) { if (vars["mode"] == GetModeVar(Mode::OP) && vars["binary"] == "false") { return "deserialize"; } return "deserializeBinary"; } string GetSerializeMethodName(std::map vars) { if (vars["mode"] == GetModeVar(Mode::OP) && vars["binary"] == "false") { return "serialize"; } return "serializeBinary"; } std::string GetSerializeMethodReturnType(std::map vars) { if (vars["mode"] == GetModeVar(Mode::OP) && vars["binary"] == "false") { return "string"; } return "!Uint8Array"; } string LowercaseFirstLetter(string s) { if (s.empty()) { return s; } s[0] = ::tolower(s[0]); return s; } string Lowercase(string s) { if (s.empty()) { return s; } for (size_t i = 0; i < s.size(); i++) { s[i] = ::tolower(s[i]); } return s; } string UppercaseFirstLetter(string s) { if (s.empty()) { return s; } s[0] = ::toupper(s[0]); return s; } string Uppercase(string s) { if (s.empty()) { return s; } for (size_t i = 0; i < s.size(); i++) { s[i] = ::toupper(s[i]); } return s; } // The following 5 functions were copied from // google/protobuf/src/google/protobuf/stubs/strutil.h inline bool HasPrefixString(const string& str, const string& prefix) { return str.size() >= prefix.size() && str.compare(0, prefix.size(), prefix) == 0; } // Strips the given prefix from the string, as well as the remaining leading dot // if it exists. inline string StripPrefixString(const string& str, const string& prefix) { if (!HasPrefixString(str, prefix)) { return str; } string remaining_str = str.substr(prefix.size()); if (!remaining_str.empty() && remaining_str[0] == '.') { remaining_str = remaining_str.substr(1); } return remaining_str; } inline bool HasSuffixString(const string& str, const string& suffix) { return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; } inline string StripSuffixString(const string& str, const string& suffix) { if (HasSuffixString(str, suffix)) { return str.substr(0, str.size() - suffix.size()); } else { return str; } } void ReplaceCharacters(string* s, const char* remove, char replacewith) { const char* str_start = s->c_str(); const char* str = str_start; for (str = strpbrk(str, remove); str != nullptr; str = strpbrk(str + 1, remove)) { (*s)[str - str_start] = replacewith; } } // The following function was copied from // google/protobuf/src/google/protobuf/compiler/cpp/cpp_helpers.cc string StripProto(const string& filename) { if (HasSuffixString(filename, ".protodevel")) { return StripSuffixString(filename, ".protodevel"); } else { return StripSuffixString(filename, ".proto"); } } // The following 6 functions were copied from // google/protobuf/src/google/protobuf/compiler/js/js_generator.cc char ToLowerASCII(char c) { if (c >= 'A' && c <= 'Z') { return (c - 'A') + 'a'; } else { return c; } } std::vector ParseLowerUnderscore(const string& input) { std::vector words; string running = ""; for (size_t i = 0; i < input.size(); i++) { if (input[i] == '_') { if (!running.empty()) { words.push_back(running); running.clear(); } } else { running += ToLowerASCII(input[i]); } } if (!running.empty()) { words.push_back(running); } return words; } string ToUpperCamel(const std::vector& words) { string result; for (size_t i = 0; i < words.size(); i++) { string word = words[i]; if (word[0] >= 'a' && word[0] <= 'z') { word[0] = (word[0] - 'a') + 'A'; } result += word; } return result; } // Returns the alias we assign to the module of the given .proto filename // when importing. string ModuleAlias(const string& filename) { // This scheme could technically cause problems if a file includes any 2 of: // foo/bar_baz.proto // foo_bar_baz.proto // foo_bar/baz.proto // // We'll worry about this problem if/when we actually see it. This name isn't // exposed to users so we can change it later if we need to. string basename = StripProto(filename); ReplaceCharacters(&basename, "-", '$'); ReplaceCharacters(&basename, "/", '_'); ReplaceCharacters(&basename, ".", '_'); return basename + "_pb"; } string JSMessageType(const Descriptor* desc, const FileDescriptor* file) { string class_name = StripPrefixString(desc->full_name(), desc->file()->package()); if (desc->file() == file) { // [for protobuf .d.ts files only] Do not add the module prefix for local // messages. return class_name; } return ModuleAlias(desc->file()->name()) + "." + class_name; } string JSMessageType(const Descriptor* desc) { return JSMessageType(desc, nullptr); } string JSElementType(const FieldDescriptor* desc, const FileDescriptor* file) { switch (desc->type()) { case FieldDescriptor::TYPE_DOUBLE: case FieldDescriptor::TYPE_FLOAT: case FieldDescriptor::TYPE_INT32: case FieldDescriptor::TYPE_UINT32: case FieldDescriptor::TYPE_SINT32: case FieldDescriptor::TYPE_FIXED32: case FieldDescriptor::TYPE_SFIXED32: return "number"; case FieldDescriptor::TYPE_INT64: case FieldDescriptor::TYPE_UINT64: case FieldDescriptor::TYPE_SINT64: case FieldDescriptor::TYPE_FIXED64: case FieldDescriptor::TYPE_SFIXED64: if (desc->options().jstype() == FieldOptions::JS_STRING) { return "string"; } else { return "number"; } case FieldDescriptor::TYPE_BOOL: return "boolean"; case FieldDescriptor::TYPE_STRING: return "string"; case FieldDescriptor::TYPE_BYTES: return "Uint8Array | string"; case FieldDescriptor::TYPE_ENUM: if (desc->enum_type()->file() == file) { // [for protobuf .d.ts files only] Do not add the module prefix for // local messages. return StripPrefixString(desc->enum_type()->full_name(), desc->enum_type()->file()->package()); } return ModuleAlias(desc->enum_type()->file()->name()) + "." + StripPrefixString(desc->enum_type()->full_name(), desc->enum_type()->file()->package()); case FieldDescriptor::TYPE_MESSAGE: return JSMessageType(desc->message_type(), file); default: return "{}"; } } string JSFieldType(const FieldDescriptor* desc, const FileDescriptor* file) { string js_field_type = JSElementType(desc, file); if (desc->is_map()) { string key_type = JSFieldType(desc->message_type()->field(0), file); string value_type = JSFieldType(desc->message_type()->field(1), file); return "jspb.Map<" + key_type + ", " + value_type + ">"; } if (desc->is_repeated()) { return "Array<" + js_field_type + ">"; } return js_field_type; } string AsObjectFieldType(const FieldDescriptor* desc, const FileDescriptor* file) { if (desc->type() != FieldDescriptor::TYPE_MESSAGE) { return JSFieldType(desc, file); } if (desc->is_map()) { const Descriptor* message = desc->message_type(); string key_type = AsObjectFieldType(message->field(0), file); string value_type = AsObjectFieldType(message->field(1), file); return "Array<[" + key_type + ", " + value_type + "]>"; } string field_type = JSMessageType(desc->message_type(), file) + ".AsObject"; if (desc->is_repeated()) { return "Array<" + field_type + ">"; } return field_type; } string JSElementName(const FieldDescriptor* desc) { return ToUpperCamel(ParseLowerUnderscore(desc->name())); } string JSFieldName(const FieldDescriptor* desc) { string js_field_name = JSElementName(desc); if (desc->is_map()) { js_field_name += "Map"; } else if (desc->is_repeated()) { js_field_name += "List"; } return js_field_name; } // Like ToUpperCamel except the first letter is not converted. string ToCamelCase(const std::vector& words) { if (words.empty()) { return ""; } string result = words[0]; return result + ToUpperCamel(std::vector( words.begin() + 1, words.begin() + words.size())); } // Like JSFieldName, but with first letter not uppercased string CamelCaseJSFieldName(const FieldDescriptor* desc) { string js_field_name = ToCamelCase(ParseLowerUnderscore(desc->name())); if (desc->is_map()) { js_field_name += "Map"; } else if (desc->is_repeated()) { js_field_name += "List"; } return js_field_name; } // Returns the name of the message with a leading dot and taking into account // nesting, for example ".OuterMessage.InnerMessage", or returns empty if // descriptor is null. This function does not handle namespacing, only message // nesting. string GetNestedMessageName(const Descriptor* descriptor) { if (descriptor == nullptr) { return ""; } string result = StripPrefixString(descriptor->full_name(), descriptor->file()->package()); // Add a leading dot if one is not already present. if (!result.empty() && result[0] != '.') { result = "." + result; } return result; } // Given a filename like foo/bar/baz.proto, returns the root directory // path ../../ string GetRootPath(const string& from_filename, const string& to_filename) { if (HasPrefixString(to_filename, "google/protobuf")) { // Well-known types (.proto files in the google/protobuf directory) are // assumed to come from the 'google-protobuf' npm package. We may want to // generalize this exception later by letting others put generated code in // their own npm packages. return "google-protobuf/"; } size_t slashes = std::count(from_filename.begin(), from_filename.end(), '/'); if (slashes == 0) { return "./"; } string result = ""; for (size_t i = 0; i < slashes; i++) { result += "../"; } return result; } // Splits path immediately following the final slash, separating it into a // directory and file name component. Directory will contain the last // slash, if it's not empty. // If there is no slash in path, Split returns an empty directory and // basename set to path. // Output values have the property that path = directory + basename. void PathSplit(const string& path, string* directory, string* basename) { string::size_type last_slash = path.rfind('/'); if (last_slash == string::npos) { if (directory) { *directory = ""; } if (basename) { *basename = path; } } else { if (directory) { *directory = path.substr(0, last_slash + 1); } if (basename) { *basename = path.substr(last_slash + 1); } } } // Returns the basename of a file. string GetBasename(string filename) { string basename; PathSplit(filename, nullptr, &basename); return basename; } //Adds $ suffix to reserved method names to avoid conflicts. static bool IsReservedMethodName(const std::string& name) { static const std::unordered_set reserved = { "extension", "jspbmessageid" }; std::string lower_name = Lowercase(name); return reserved.count(lower_name) > 0; } static std::string SafeAccessorName(const std::string& name) { std::string result = name; if (IsReservedMethodName(name)) { result += "$"; } return result; } // Finds all message types used in all services in the file. Return results as a // map of full names to descriptors to get sorted results and deterministic // build outputs. std::map GetAllMessages(const FileDescriptor* file) { std::map messages; for (int s = 0; s < file->service_count(); ++s) { const ServiceDescriptor* service = file->service(s); for (int m = 0; m < service->method_count(); ++m) { const MethodDescriptor* method = service->method(m); messages[method->input_type()->full_name()] = method->input_type(); messages[method->output_type()->full_name()] = method->output_type(); } } return messages; } void PrintClosureDependencies(Printer* printer, const FileDescriptor* file) { for (const auto& entry : GetAllMessages(file)) { printer->Print("goog.require('proto.$full_name$');\n", "full_name", entry.second->full_name()); } } void PrintCommonJsMessagesDeps(Printer* printer, const FileDescriptor* file) { std::map vars; for (int i = 0; i < file->dependency_count(); i++) { const string& name = file->dependency(i)->name(); vars["alias"] = ModuleAlias(name); vars["dep_filename"] = GetRootPath(file->name(), name) + StripProto(name); // we need to give each cross-file import an alias printer->Print(vars, "\nvar $alias$ = require('$dep_filename$_pb.js')\n"); } const string& package = file->package(); vars["package_name"] = package; if (!package.empty()) { size_t offset = 0; size_t dotIndex = package.find('.'); printer->Print(vars, "const proto = {};\n"); while (dotIndex != string::npos) { vars["current_package_ns"] = package.substr(0, dotIndex); printer->Print(vars, "proto.$current_package_ns$ = {};\n"); offset = dotIndex + 1; dotIndex = package.find('.', offset); } } // need to import the messages from our own file vars["filename"] = GetBasename(StripProto(file->name())); if (!package.empty()) { printer->Print(vars, "proto.$package_name$ = require('./$filename$_pb.js');\n\n"); } else { printer->Print(vars, "const proto = require('./$filename$_pb.js');\n\n"); } } void PrintES6Imports(Printer* printer, const FileDescriptor* file) { std::map vars; printer->Print("import * as grpcWeb from 'grpc-web';\n\n"); std::set imports; for (const auto& entry : GetAllMessages(file)) { const string& proto_filename = entry.second->file()->name(); string dep_filename = GetRootPath(file->name(), proto_filename) + StripProto(proto_filename); if (imports.find(dep_filename) != imports.end()) { continue; } imports.insert(dep_filename); // We need to give each cross-file import an alias. printer->Print("import * as $alias$ from '$dep_filename$_pb'; // proto import: \"$proto_filename$\"\n", "alias", ModuleAlias(proto_filename), "dep_filename", dep_filename, "proto_filename", proto_filename); } printer->Print("\n\n"); } void PrintTypescriptFile(Printer* printer, const FileDescriptor* file, std::map vars) { PrintES6Imports(printer, file); for (int service_index = 0; service_index < file->service_count(); ++service_index) { printer->Print("export class "); const ServiceDescriptor* service = file->service(service_index); vars["service_name"] = service->name(); printer->Print(vars, "$service_name$Client {\n"); printer->Indent(); printer->Print( "client_: grpcWeb.AbstractClientBase;\n" "hostname_: string;\n" "credentials_: null | { [index: string]: string; };\n" "options_: null | { [index: string]: any; };\n\n" "constructor (hostname: string,\n" " credentials?: null | { [index: string]: string; },\n" " options?: null | { [index: string]: any; }) {\n"); printer->Indent(); printer->Print("if (!options) options = {};\n"); printer->Print("if (!credentials) credentials = {};\n"); if (vars["mode"] == GetModeVar(Mode::GRPCWEB)) { printer->Print(vars, "options['format'] = '$format$';\n\n"); } printer->Print(vars, "this.client_ = new grpcWeb.$mode$ClientBase(options);\n" "this.hostname_ = hostname.replace(/\\/+$$/, '');\n" "this.credentials_ = credentials;\n" "this.options_ = options;\n"); printer->Outdent(); printer->Print("}\n\n"); for (int method_index = 0; method_index < service->method_count(); ++method_index) { const MethodDescriptor* method = service->method(method_index); vars["js_method_name"] = LowercaseFirstLetter(method->name()); vars["method_name"] = method->name(); vars["input_type"] = JSMessageType(method->input_type()); vars["output_type"] = JSMessageType(method->output_type()); vars["serialize_func_name"] = GetSerializeMethodName(vars); vars["deserialize_func_name"] = GetDeserializeMethodName(vars); vars["method_type"] = method->server_streaming() ? "grpcWeb.MethodType.SERVER_STREAMING" : "grpcWeb.MethodType.UNARY"; if (!method->client_streaming()) { printer->Print(vars, "methodDescriptor$method_name$ = " "new grpcWeb.MethodDescriptor(\n"); printer->Indent(); printer->Print(vars, "'/$package_dot$$service_name$/$method_name$',\n" "$method_type$,\n" "$input_type$,\n" "$output_type$,\n" "(request: $input_type$) => {\n" " return request.$serialize_func_name$();\n" "},\n" "$output_type$.$deserialize_func_name$\n"); printer->Outdent(); printer->Print(");\n\n"); if (method->server_streaming()) { printer->Print(vars, "$js_method_name$(\n"); printer->Indent(); printer->Print(vars, "request: $input_type$,\n" "metadata?: grpcWeb.Metadata): " "grpcWeb.ClientReadableStream<$output_type$> {\n"); printer->Print(vars, "return this.client_.serverStreaming(\n"); printer->Indent(); printer->Print(vars, "this.hostname_ +\n" " '/$package_dot$$service_name$/$method_name$',\n" "request,\n" "metadata || {},\n" "this.methodDescriptor$method_name$);\n"); printer->Outdent(); printer->Outdent(); printer->Print("}\n\n"); } else { printer->Print(vars, "$js_method_name$(\n"); printer->Indent(); printer->Print(vars, "request: $input_type$,\n" "metadata?: grpcWeb.Metadata | null): " "$promise$<$output_type$>;\n\n"); printer->Outdent(); printer->Print(vars, "$js_method_name$(\n"); printer->Indent(); printer->Print(vars, "request: $input_type$,\n" "metadata: grpcWeb.Metadata | null,\n" "callback: (err: grpcWeb.RpcError,\n" " response: $output_type$) => void): " "grpcWeb.ClientReadableStream<$output_type$>;\n\n"); printer->Outdent(); printer->Print(vars, "$js_method_name$(\n"); printer->Indent(); printer->Print(vars, "request: $input_type$,\n" "metadata?: grpcWeb.Metadata | null,\n" "callback?: (err: grpcWeb.RpcError,\n" " response: $output_type$) => void) {\n"); printer->Print(vars, "if (callback !== undefined) {\n"); printer->Indent(); printer->Print(vars, "return this.client_.rpcCall(\n"); printer->Indent(); printer->Print(vars, "this.hostname_ +\n" " '/$package_dot$$service_name$/$method_name$',\n" "request,\n" "metadata || {},\n" "this.methodDescriptor$method_name$,\n" "callback);\n"); printer->Outdent(); printer->Outdent(); printer->Print(vars, "}\n" "return this.client_.unaryCall(\n"); printer->Print(vars, "this.hostname_ +\n" " '/$package_dot$$service_name$/$method_name$',\n" "request,\n" "metadata || {},\n" "this.methodDescriptor$method_name$);\n"); printer->Outdent(); printer->Print("}\n\n"); } } } printer->Outdent(); printer->Print("}\n\n"); } } void PrintGrpcWebDtsClientClass(Printer* printer, const FileDescriptor* file, const string& client_type) { std::map vars; vars["client_type"] = client_type; vars["promise"] = "Promise"; for (int service_index = 0; service_index < file->service_count(); ++service_index) { printer->Print("export class "); const ServiceDescriptor* service = file->service(service_index); vars["service_name"] = service->name(); printer->Print(vars, "$service_name$$client_type$ {\n"); printer->Indent(); printer->Print( "constructor (hostname: string,\n" " credentials?: null | { [index: string]: string; },\n" " options?: null | { [index: string]: any; });\n\n"); for (int method_index = 0; method_index < service->method_count(); ++method_index) { const MethodDescriptor* method = service->method(method_index); vars["js_method_name"] = LowercaseFirstLetter(method->name()); vars["input_type"] = JSMessageType(method->input_type()); vars["output_type"] = JSMessageType(method->output_type()); if (!method->client_streaming()) { if (method->server_streaming()) { printer->Print(vars, "$js_method_name$(\n"); printer->Indent(); printer->Print(vars, "request: $input_type$,\n" "metadata?: grpcWeb.Metadata\n"); printer->Outdent(); printer->Print(vars, "): grpcWeb.ClientReadableStream<$output_type$>;\n\n"); } else { if (vars["client_type"] == "PromiseClient") { printer->Print(vars, "$js_method_name$(\n"); printer->Indent(); printer->Print(vars, "request: $input_type$,\n" "metadata?: grpcWeb.Metadata\n"); printer->Outdent(); printer->Print(vars, "): $promise$<$output_type$>;\n\n"); } else { printer->Print(vars, "$js_method_name$(\n"); printer->Indent(); printer->Print(vars, "request: $input_type$,\n" "metadata: grpcWeb.Metadata | undefined,\n" "callback: (err: grpcWeb.RpcError,\n" " response: $output_type$) => void\n"); printer->Outdent(); printer->Print(vars, "): grpcWeb.ClientReadableStream<$output_type$>;"); printer->Print("\n\n"); } } } } printer->Outdent(); printer->Print("}\n\n"); } } void PrintGrpcWebDtsFile(Printer* printer, const FileDescriptor* file) { PrintES6Imports(printer, file); PrintGrpcWebDtsClientClass(printer, file, "Client"); PrintGrpcWebDtsClientClass(printer, file, "PromiseClient"); } void PrintProtoDtsEnum(Printer* printer, const EnumDescriptor* desc) { std::map vars; vars["enum_name"] = desc->name(); // Use regular enums for broad TypeScript compatibility. `const enum` // triggers TS2748 when `verbatimModuleSyntax` is enabled (default in // TypeScript 5.9+), so prefer `enum` here. printer->Print(vars, "export enum $enum_name$ {\n"); printer->Indent(); for (int i = 0; i < desc->value_count(); i++) { vars["value_name"] = Uppercase(desc->value(i)->name()); vars["value_number"] = std::to_string(desc->value(i)->number()); printer->Print(vars, "$value_name$ = $value_number$,\n"); } printer->Outdent(); printer->Print("}\n"); } void PrintProtoDtsOneofCase(Printer* printer, const OneofDescriptor* desc) { std::map vars; vars["oneof_name"] = ToUpperCamel(ParseLowerUnderscore(desc->name())); vars["oneof_name_upper"] = Uppercase(desc->name()); // Oneof case enums also use regular enums to avoid ambient `const enum` // issues under `verbatimModuleSyntax`. printer->Print(vars, "export enum $oneof_name$Case {\n"); printer->Indent(); printer->Print(vars, "$oneof_name_upper$_NOT_SET = 0,\n"); for (int i = 0; i < desc->field_count(); i++) { const FieldDescriptor* field = desc->field(i); vars["field_name"] = Uppercase(field->name()); vars["field_number"] = std::to_string(field->number()); printer->Print(vars, "$field_name$ = $field_number$,\n"); } printer->Outdent(); printer->Print("}\n"); } void PrintProtoDtsMessage(Printer* printer, const Descriptor* desc, const FileDescriptor* file) { const string& class_name = desc->name(); std::map vars; vars["class_name"] = class_name; printer->Print(vars, "export class $class_name$ extends jspb.Message {\n"); printer->Indent(); printer->Print(vars, "constructor();\n" "constructor(opt_data?: $class_name$.AsObject);\n"); for (int i = 0; i < desc->field_count(); i++) { const FieldDescriptor* field = desc->field(i); vars["js_field_name"] = SafeAccessorName(JSFieldName(field)); vars["js_field_type"] = JSFieldType(field, file); if (field->type() != FieldDescriptor::TYPE_MESSAGE || field->is_repeated()) { printer->Print(vars, "get$js_field_name$(): $js_field_type$;\n"); } else { printer->Print(vars, "get$js_field_name$(): $js_field_type$ | undefined;\n"); } if (field->type() == FieldDescriptor::TYPE_BYTES && !field->is_repeated()) { printer->Print(vars, "get$js_field_name$_asU8(): Uint8Array;\n" "get$js_field_name$_asB64(): string;\n"); } if (!field->is_map() && (field->type() != FieldDescriptor::TYPE_MESSAGE || field->is_repeated())) { printer->Print(vars, "set$js_field_name$(value: $js_field_type$): " "$class_name$;\n"); } else if (!field->is_map()) { printer->Print(vars, "set$js_field_name$(value?: $js_field_type$): " "$class_name$;\n"); } if (field->has_presence()) { printer->Print(vars, "has$js_field_name$(): boolean;\n"); } if (field->type() == FieldDescriptor::TYPE_MESSAGE || field->has_presence() || field->is_repeated() || field->is_map()) { printer->Print(vars, "clear$js_field_name$(): $class_name$;\n"); } if (field->is_repeated() && !field->is_map()) { vars["js_field_name"] = SafeAccessorName(JSElementName(field)); vars["js_field_type"] = JSElementType(field, file); if (field->type() != FieldDescriptor::TYPE_MESSAGE) { printer->Print(vars, "add$js_field_name$(value: $js_field_type$, " "index?: number): $class_name$;\n"); } else { printer->Print(vars, "add$js_field_name$(value?: $js_field_type$, " "index?: number): $js_field_type$;\n"); } } printer->Print("\n"); } for (int i = 0; i < desc->real_oneof_decl_count(); i++) { const OneofDescriptor *oneof = desc->real_oneof_decl(i); vars["js_oneof_name"] = ToUpperCamel(ParseLowerUnderscore(oneof->name())); printer->Print( vars, "get$js_oneof_name$Case(): $class_name$.$js_oneof_name$Case;\n"); printer->Print("\n"); } printer->Print( vars, "serializeBinary(): Uint8Array;\n" "toObject(includeInstance?: boolean): " "$class_name$.AsObject;\n" "static toObject(includeInstance: boolean, msg: $class_name$): " "$class_name$.AsObject;\n" "static serializeBinaryToWriter(message: $class_name$, writer: " "jspb.BinaryWriter): void;\n" "static deserializeBinary(bytes: Uint8Array): $class_name$;\n" "static deserializeBinaryFromReader(message: $class_name$, reader: " "jspb.BinaryReader): $class_name$;\n"); printer->Outdent(); printer->Print("}\n\n"); printer->Print(vars, "export namespace $class_name$ {\n"); printer->Indent(); printer->Print("export type AsObject = {\n"); printer->Indent(); for (int i = 0; i < desc->field_count(); i++) { const FieldDescriptor* field = desc->field(i); string js_field_name = CamelCaseJSFieldName(field); if (IsReserved(js_field_name)) { js_field_name = "pb_" + js_field_name; } vars["js_field_name"] = js_field_name; vars["js_field_type"] = AsObjectFieldType(field, file); if (!field->has_presence()) { printer->Print(vars, "$js_field_name$: $js_field_type$;\n"); } else { printer->Print(vars, "$js_field_name$?: $js_field_type$;\n"); } } printer->Outdent(); printer->Print("};\n"); for (int i = 0; i < desc->nested_type_count(); i++) { if (desc->nested_type(i)->options().map_entry()) { continue; } printer->Print("\n"); PrintProtoDtsMessage(printer, desc->nested_type(i), file); } for (int i = 0; i < desc->enum_type_count(); i++) { printer->Print("\n"); PrintProtoDtsEnum(printer, desc->enum_type(i)); } for (int i = 0; i < desc->oneof_decl_count(); i++) { printer->Print("\n"); PrintProtoDtsOneofCase(printer, desc->oneof_decl(i)); } printer->Outdent(); printer->Print("}\n\n"); } void PrintProtoDtsFile(Printer* printer, const FileDescriptor* file) { printer->Print("import * as jspb from 'google-protobuf'\n\n"); for (int i = 0; i < file->dependency_count(); i++) { const string& proto_filename = file->dependency(i)->name(); // We need to give each cross-file import an alias. printer->Print("import * as $alias$ from '$dep_filename$_pb'; // proto import: \"$proto_filename$\"\n", "alias", ModuleAlias(proto_filename), "dep_filename", GetRootPath(file->name(), proto_filename) + StripProto(proto_filename), "proto_filename", proto_filename); } printer->Print("\n\n"); for (int i = 0; i < file->message_type_count(); i++) { PrintProtoDtsMessage(printer, file->message_type(i), file); } for (int i = 0; i < file->enum_type_count(); i++) { PrintProtoDtsEnum(printer, file->enum_type(i)); } } void PrintFileHeader(Printer* printer, const std::map& vars) { printer->Print( vars, "/**\n" " * @fileoverview gRPC-Web generated client stub for $package$\n" " * @enhanceable\n" " * @public\n" " */\n\n" "// Code generated by protoc-gen-grpc-web. DO NOT EDIT.\n" "// versions:\n" "// \tprotoc-gen-grpc-web v$version$\n" "// \tprotoc v$protoc_version$\n" "// source: $source_file$\n\n\n" "/* eslint-disable */\n" "// @ts-nocheck\n\n\n"); } void PrintMethodDescriptorFile(Printer* printer, std::map vars) { printer->Print( vars, "/**\n" " * @fileoverview gRPC-Web generated MethodDescriptors for $package$\n"); if (vars["plugins"].empty()) { printer->Print(" * @enhanceable\n"); } printer->Print( " * @public\n" " */\n\n" "// Code generated by protoc-gen-grpc-web. DO NOT EDIT.\n" "// versions:\n" "// \tprotoc-gen-grpc-web v$version$\n" "// \tprotoc v$protoc_version$\n" "// source: $source_file$\n\n\n" "/* eslint-disable */\n" "// @ts-nocheck\n\n\n"); printer->Print(vars, "goog.provide('proto.$package_dot$$class_name$.$" "method_name$MethodDescriptor');\n\n"); if (!vars["plugins"].empty()) { printer->Print(vars, "goog.require('$plugins$.$package_dot$$class_name$.$" "method_name$MethodDescriptor');\n"); } printer->Print(vars, "goog.require('grpc.web.MethodDescriptor');\n"); printer->Print(vars, "goog.require('grpc.web.MethodType');\n"); printer->Print(vars, "goog.require('$in_type$');\n"); if (vars["out_type"] != vars["in_type"]) { printer->Print(vars, "goog.require('$out_type$');\n"); } printer->Print(vars, "\n\ngoog.scope(function() {\n\n"); printer->Print( vars, "/**\n" " * @const\n" " * @type {!grpc.web.MethodDescriptor<\n" " * !proto.$in$,\n" " * !proto.$out$>}\n" " */\n" "proto.$package_dot$$class_name$.$method_name$MethodDescriptor = \n"); printer->Indent(); printer->Indent(); printer->Print(vars, "new grpc.web.MethodDescriptor(\n"); printer->Indent(); printer->Indent(); printer->Print(vars, "'/$package_dot$$service_name$/$method_name$',\n" "$method_type$,\n" "$in_type$,\n"); printer->Print(vars, "$out_type$,\n" "/**\n" " * @param {!proto.$in$} request\n"); printer->Print( (" * @return {" + GetSerializeMethodReturnType(vars) + "}\n").c_str()); printer->Print( " */\n" "function(request) {\n"); printer->Print( (" return request." + GetSerializeMethodName(vars) + "();\n").c_str()); printer->Print("},\n"); printer->Print(vars, ("$out_type$." + GetDeserializeMethodName(vars)).c_str()); printer->Print(vars, ");\n\n\n"); printer->Outdent(); printer->Outdent(); printer->Outdent(); printer->Outdent(); printer->Print("}); // goog.scope\n\n"); } void PrintServiceConstructor(Printer* printer, std::map vars, bool is_promise) { vars["is_promise"] = is_promise ? "Promise" : ""; printer->Print(vars, "/**\n" " * @param {string} hostname\n" " * @param {?Object} credentials\n" " * @param {?grpc.web.ClientOptions} options\n" " * @constructor\n" " * @struct\n" " * @final\n" " */\n" "proto.$package_dot$$service_name$$is_promise$Client =\n" " function(hostname, credentials, options) {\n" " if (!options) options = {};\n"); if (vars["mode"] == GetModeVar(Mode::GRPCWEB)) { printer->Print(vars, " options.format = '$format$';\n\n"); } if (vars["mode"] == GetModeVar(Mode::OP)) { printer->Print( vars, " /**\n" " * @private @const {!grpc.web.$mode$ClientBase} The client\n" " */\n" " this.client_ = new grpc.web.$mode$ClientBase(options, " "$binary$);\n\n"); } else { printer->Print( vars, " /**\n" " * @private @const {!grpc.web.$mode$ClientBase} The client\n" " */\n" " this.client_ = new grpc.web.$mode$ClientBase(options);\n\n"); } printer->PrintRaw( " /**\n" " * @private @const {string} The hostname\n" " */\n" " this.hostname_ = hostname.replace(/\\/+$/, '');\n\n" "};\n\n\n"); } void PrintMethodDescriptor(Printer* printer, std::map vars) { printer->Print(vars, "/**\n" " * @const\n" " * @type {!grpc.web.MethodDescriptor<\n" " * !proto.$in$,\n" " * !proto.$out$>}\n" " */\n" "const methodDescriptor_$service_name$_$method_name$ = " "new grpc.web.MethodDescriptor(\n"); printer->Indent(); printer->Print(vars, "'/$package_dot$$service_name$/$method_name$',\n" "$method_type$,\n" "$in_type$,\n"); printer->Print(vars, "$out_type$,\n" "/**\n" " * @param {!proto.$in$} request\n"); printer->Print( (" * @return {" + GetSerializeMethodReturnType(vars) + "}\n").c_str()); printer->Print( " */\n" "function(request) {\n"); printer->Print( (" return request." + GetSerializeMethodName(vars) + "();\n").c_str()); printer->Print("},\n"); printer->Print( vars, ("$out_type$." + GetDeserializeMethodName(vars) + "\n").c_str()); printer->Outdent(); printer->Print(vars, ");\n\n\n"); } void PrintUnaryCall(Printer* printer, std::map vars) { printer->Print( vars, "/**\n" " * @param {!proto.$in$} request The\n" " * request proto\n" " * @param {?Object} metadata User defined\n" " * call metadata\n" " * @param {function(?grpc.web.RpcError," " ?proto.$out$)}\n" " * callback The callback function(error, response)\n" " * @return {!grpc.web.ClientReadableStream|undefined}\n" " * The XHR Node Readable Stream\n" " */\n" "proto.$package_dot$$service_name$Client.prototype.$js_method_name$ =\n"); printer->Indent(); printer->Print(vars, " function(request, metadata, callback) {\n" "return this.client_.rpcCall(this.hostname_ +\n"); printer->Indent(); printer->Indent(); if (vars["mode"] == GetModeVar(Mode::OP)) { printer->Print(vars, "'/$$rpc/$package_dot$$service_name$/$method_name$',\n"); } else { printer->Print(vars, "'/$package_dot$$service_name$/$method_name$',\n"); } printer->Print(vars, "request,\n" "metadata || {},\n" "$method_descriptor$,\n" "callback);\n"); printer->Outdent(); printer->Outdent(); printer->Outdent(); printer->Print("};\n\n\n"); } void PrintPromiseUnaryCall(Printer* printer, std::map vars) { printer->Print(vars, "/**\n" " * @param {!proto.$in$} request The\n" " * request proto\n" " * @param {?Object=} metadata User defined\n" " * call metadata\n" " * @return {!$promise$}\n" " * Promise that resolves to the response\n" " */\n" "proto.$package_dot$$service_name$PromiseClient.prototype" ".$js_method_name$ =\n"); printer->Indent(); printer->Print(vars, " function(request, metadata) {\n" "return this.client_.unaryCall(this.hostname_ +\n"); printer->Indent(); printer->Indent(); if (vars["mode"] == GetModeVar(Mode::OP)) { printer->Print(vars, "'/$$rpc/$package_dot$$service_name$/$method_name$',\n"); } else { printer->Print(vars, "'/$package_dot$$service_name$/$method_name$',\n"); } printer->Print(vars, "request,\n" "metadata || {},\n" "$method_descriptor$);\n"); printer->Outdent(); printer->Outdent(); printer->Outdent(); printer->Print("};\n\n\n"); } void PrintServerStreamingCall(Printer* printer, std::map vars) { printer->Print(vars, "/**\n" " * @param {!proto.$in$} request The request proto\n" " * @param {?Object=} metadata User defined\n" " * call metadata\n" " * @return {!grpc.web.ClientReadableStream}\n" " * The XHR Node Readable Stream\n" " */\n" "proto.$package_dot$$service_name$$client_type$.prototype." "$js_method_name$ =\n"); printer->Indent(); printer->Print( " function(request, metadata) {\n" "return this.client_.serverStreaming(this.hostname_ +\n"); printer->Indent(); printer->Indent(); if (vars["mode"] == GetModeVar(Mode::OP)) { printer->Print(vars, "'/$$rpc/$package_dot$$service_name$/$method_name$',\n"); } else { printer->Print(vars, "'/$package_dot$$service_name$/$method_name$',\n"); } printer->Print(vars, "request,\n" "metadata || {},\n" "$method_descriptor$);\n"); printer->Outdent(); printer->Outdent(); printer->Outdent(); printer->Print("};\n\n\n"); } void PrintMultipleFilesMode(const FileDescriptor* file, string file_name, GeneratorContext* context, std::map vars) { std::map method_descriptors; bool has_server_streaming = false; // Print MethodDescriptor files. for (int i = 0; i < file->service_count(); ++i) { const ServiceDescriptor* service = file->service(i); vars["service_name"] = service->name(); vars["class_name"] = LowercaseFirstLetter(service->name()); for (int method_index = 0; method_index < service->method_count(); ++method_index) { const MethodDescriptor* method = service->method(method_index); string method_file_name = Lowercase(service->name()) + "_" + Lowercase(method->name()) + "_methoddescriptor.js"; if (method->server_streaming()) { has_server_streaming = true; } std::unique_ptr output( context->Open(method_file_name)); Printer printer(output.get(), '$'); vars["method_name"] = method->name(); vars["in"] = method->input_type()->full_name(); vars["in_type"] = "proto." + method->input_type()->full_name(); vars["out"] = method->output_type()->full_name(); vars["out_type"] = "proto." + method->output_type()->full_name(); vars["method_type"] = method->server_streaming() ? "grpc.web.MethodType.SERVER_STREAMING" : "grpc.web.MethodType.UNARY"; PrintMethodDescriptorFile(&printer, vars); method_descriptors[service->name() + "." + method->name()] = "proto." + vars["package_dot"] + vars["class_name"] + "." + vars["method_name"] + "MethodDescriptor"; } } std::unique_ptr output1( context->Open(file_name + "_client_pb.js")); Printer printer1(output1.get(), '$'); std::unique_ptr output2( context->Open(file_name + "_promise_client_pb.js")); Printer printer2(output2.get(), '$'); PrintFileHeader(&printer1, vars); PrintFileHeader(&printer2, vars); // Print the Promise and callback client. for (int i = 0; i < file->service_count(); ++i) { const ServiceDescriptor* service = file->service(i); vars["service_name"] = service->name(); printer1.Print(vars, "goog.provide('proto.$package_dot$$service_name$" "Client');\n\n"); printer2.Print(vars, "goog.provide('proto.$package_dot$$service_name$" "PromiseClient');\n\n"); } if (vars["promise"] == GRPC_PROMISE) { printer2.Print(vars, "goog.require('grpc.web.promise');\n"); } std::map::iterator it; for (it = method_descriptors.begin(); it != method_descriptors.end(); it++) { vars["import_mtd"] = it->second; printer1.Print(vars, "goog.require('$import_mtd$');\n"); printer2.Print(vars, "goog.require('$import_mtd$');\n"); } printer1.Print(vars, "goog.require('grpc.web.$mode$ClientBase');\n"); printer1.Print(vars, "goog.require('grpc.web.ClientReadableStream');\n"); printer1.Print(vars, "goog.require('grpc.web.RpcError');\n"); printer2.Print(vars, "goog.require('grpc.web.$mode$ClientBase');\n"); if (has_server_streaming) { printer2.Print(vars, "goog.require('grpc.web.ClientReadableStream');\n"); } PrintClosureDependencies(&printer1, file); PrintClosureDependencies(&printer2, file); printer1.Print(vars, "\ngoog.requireType('grpc.web.ClientOptions');\n"); printer2.Print(vars, "\ngoog.requireType('grpc.web.ClientOptions');\n"); printer1.Print("\n\n\n"); printer2.Print("\n\n\n"); printer1.Print("goog.scope(function() {\n\n"); printer2.Print("goog.scope(function() {\n\n"); for (int service_index = 0; service_index < file->service_count(); ++service_index) { const ServiceDescriptor* service = file->service(service_index); vars["service_name"] = service->name(); PrintServiceConstructor(&printer1, vars, false); PrintServiceConstructor(&printer2, vars, true); for (int method_index = 0; method_index < service->method_count(); ++method_index) { const MethodDescriptor* method = service->method(method_index); const Descriptor* input_type = method->input_type(); const Descriptor* output_type = method->output_type(); vars["js_method_name"] = LowercaseFirstLetter(method->name()); vars["method_name"] = method->name(); vars["in"] = input_type->full_name(); vars["out"] = output_type->full_name(); vars["method_descriptor"] = method_descriptors[service->name() + "." + method->name()]; vars["in_type"] = "proto." + input_type->full_name(); vars["out_type"] = "proto." + output_type->full_name(); // Client streaming is not supported yet if (!method->client_streaming()) { if (method->server_streaming()) { vars["method_type"] = "grpc.web.MethodType.SERVER_STREAMING"; vars["client_type"] = "Client"; PrintServerStreamingCall(&printer1, vars); vars["client_type"] = "PromiseClient"; PrintServerStreamingCall(&printer2, vars); } else { vars["method_type"] = "grpc.web.MethodType.UNARY"; PrintUnaryCall(&printer1, vars); PrintPromiseUnaryCall(&printer2, vars); } } } } printer1.Print("}); // goog.scope\n\n"); printer2.Print("}); // goog.scope\n\n"); } void PrintClosureES6Imports(Printer* printer, const FileDescriptor* file, string package_dot) { for (int i = 0; i < file->service_count(); ++i) { const ServiceDescriptor* service = file->service(i); string service_namespace = "proto." + package_dot + service->name(); printer->Print( "import $service_name$Client_import from 'goog:$namespace$';\n", "service_name", service->name(), "namespace", service_namespace + "Client"); printer->Print( "import $service_name$PromiseClient_import from 'goog:$namespace$';\n", "service_name", service->name(), "namespace", service_namespace + "PromiseClient"); } printer->Print("\n\n\n"); } void PrintGrpcWebClosureES6File(Printer* printer, const FileDescriptor* file) { string package_dot = file->package().empty() ? "" : file->package() + "."; printer->Print( "/**\n" " * @fileoverview gRPC-Web generated client stub for '$file$'\n" " */\n" "\n" "// Code generated by protoc-gen-grpc-web. DO NOT EDIT.\n" "// versions:\n" "// \tprotoc-gen-grpc-web v$version$\n" "// \tprotoc v$protoc_version$\n" "// source: $source_file$\n" "\n" "\n", "file", file->name()); PrintClosureES6Imports(printer, file, package_dot); for (int i = 0; i < file->service_count(); ++i) { const ServiceDescriptor* service = file->service(i); string service_namespace = "proto." + package_dot + service->name(); printer->Print("export const $name$Client = $name$Client_import;\n", "name", service->name()); printer->Print( "export const $name$PromiseClient = $name$PromiseClient_import;\n", "name", service->name()); } } class GeneratorOptions { public: GeneratorOptions(); bool ParseFromOptions(const string& parameter, string* error); bool ParseFromOptions(const std::vector>& options, string* error); // Returns the name of the output file for |proto_file|. string OutputFile(const string& proto_file) const; string mode() const { return mode_; } string plugins() const { return plugins_; } ImportStyle import_style() const { return import_style_; } bool generate_dts() const { return generate_dts_; } bool generate_closure_es6() const { return generate_closure_es6_; } bool multiple_files() const { return multiple_files_; } bool goog_promise() const { return goog_promise_; } private: string file_name_; string mode_; string plugins_; ImportStyle import_style_; bool generate_dts_; bool generate_closure_es6_; bool multiple_files_; bool goog_promise_; }; GeneratorOptions::GeneratorOptions() : file_name_(""), mode_(""), plugins_(""), import_style_(ImportStyle::CLOSURE), generate_dts_(false), generate_closure_es6_(false), multiple_files_(false), goog_promise_(false) {} bool GeneratorOptions::ParseFromOptions(const string& parameter, string* error) { std::vector> options; ParseGeneratorParameter(parameter, &options); return ParseFromOptions(options, error); } bool GeneratorOptions::ParseFromOptions( const std::vector>& options, string* error) { for (const std::pair& option : options) { if ("out" == option.first) { file_name_ = option.second; } else if ("mode" == option.first) { mode_ = option.second; } else if ("import_style" == option.first) { if ("closure" == option.second) { import_style_ = ImportStyle::CLOSURE; } else if ("experimental_closure_es6" == option.second) { import_style_ = ImportStyle::CLOSURE; generate_closure_es6_ = true; } else if ("commonjs" == option.second) { import_style_ = ImportStyle::COMMONJS; } else if ("commonjs+dts" == option.second) { import_style_ = ImportStyle::COMMONJS; generate_dts_ = true; } else if ("typescript" == option.second) { import_style_ = ImportStyle::TYPESCRIPT; generate_dts_ = true; } else { *error = "options: invalid import_style - " + option.second; return false; } } else if ("multiple_files" == option.first) { multiple_files_ = "True" == option.second; } else if ("plugins" == option.first) { plugins_ = option.second; } else if ("goog_promise" == option.first) { goog_promise_ = "True" == option.second; } else { *error = "unsupported option: " + option.first; return false; } } if (mode_.empty()) { *error = "options: mode is required"; return false; } return true; } string GeneratorOptions::OutputFile(const string& proto_file) const { if (ImportStyle::TYPESCRIPT == import_style()) { // Never use the value from the 'out' option when generating TypeScript. string directory; string basename; PathSplit(proto_file, &directory, &basename); return directory + UppercaseFirstLetter(StripProto(basename)) + "ServiceClientPb.ts"; } if (!file_name_.empty()) { return file_name_; } return StripProto(proto_file) + "_grpc_web_pb.js"; } class GrpcCodeGenerator : public CodeGenerator { public: GrpcCodeGenerator() {} ~GrpcCodeGenerator() override {} uint64_t GetSupportedFeatures() const override { // Code generators must explicitly support proto3 optional. return CodeGenerator::FEATURE_PROTO3_OPTIONAL | CodeGenerator::FEATURE_SUPPORTS_EDITIONS; } // Keep synced with protoc-gen-js: https://github.com/protocolbuffers/protobuf-javascript/blob/861c8020a5c0cba9b7cdf915dffde96a4421a1f4/generator/js_generator.h#L157-L158 Edition GetMinimumEdition() const override { return Edition::EDITION_PROTO2; } Edition GetMaximumEdition() const override { return Edition::EDITION_2023; } bool Generate(const FileDescriptor* file, const string& parameter, GeneratorContext* context, string* error) const override { GeneratorOptions generator_options; if (!generator_options.ParseFromOptions(parameter, error)) { return false; } std::map vars; std::map method_descriptors; string package = file->package(); vars["package"] = package; vars["package_dot"] = package.empty() ? "" : package + '.'; vars["promise"] = "Promise"; vars["plugins"] = generator_options.plugins(); if ("binary" == generator_options.mode()) { vars["mode"] = GetModeVar(Mode::OP); vars["binary"] = "true"; } else if ("grpcweb" == generator_options.mode()) { vars["mode"] = GetModeVar(Mode::GRPCWEB); vars["format"] = "binary"; } else if ("grpcwebtext" == generator_options.mode()) { vars["mode"] = GetModeVar(Mode::GRPCWEB); vars["format"] = "text"; } else if ("jspb" == generator_options.mode()) { vars["mode"] = GetModeVar(Mode::OP); vars["binary"] = "false"; if (generator_options.goog_promise()) { vars["promise"] = GRPC_PROMISE; } } else { *error = "options: invalid mode - " + generator_options.mode(); return false; } if (generator_options.generate_dts()) { string proto_dts_file_name = StripProto(file->name()) + "_pb.d.ts"; std::unique_ptr proto_dts_output( context->Open(proto_dts_file_name)); Printer proto_dts_printer(proto_dts_output.get(), '$'); PrintProtoDtsFile(&proto_dts_printer, file); } if (!file->service_count()) { // No services, nothing to do. return true; } vars["version"] = GRPC_WEB_VERSION; vars["protoc_version"] = GetProtocVersion(context); vars["source_file"] = file->name(); string file_name = generator_options.OutputFile(file->name()); if (generator_options.multiple_files() && ImportStyle::CLOSURE == generator_options.import_style()) { PrintMultipleFilesMode(file, file_name, context, vars); return true; } std::unique_ptr output(context->Open(file_name)); Printer printer(output.get(), '$'); PrintFileHeader(&printer, vars); if (ImportStyle::TYPESCRIPT == generator_options.import_style()) { PrintTypescriptFile(&printer, file, vars); return true; } for (int i = 0; i < file->service_count(); ++i) { const ServiceDescriptor* service = file->service(i); vars["service_name"] = service->name(); switch (generator_options.import_style()) { case ImportStyle::CLOSURE: printer.Print( vars, "goog.provide('proto.$package_dot$$service_name$Client');\n"); printer.Print(vars, "goog.provide('proto.$package_dot$$service_name$" "PromiseClient');\n"); break; case ImportStyle::COMMONJS: break; case ImportStyle::TYPESCRIPT: break; } } printer.Print("\n"); switch (generator_options.import_style()) { case ImportStyle::CLOSURE: if (vars["promise"] == GRPC_PROMISE) { printer.Print(vars, "goog.require('grpc.web.promise');\n"); } printer.Print(vars, "goog.require('grpc.web.MethodDescriptor');\n"); printer.Print(vars, "goog.require('grpc.web.MethodType');\n"); printer.Print(vars, "goog.require('grpc.web.$mode$ClientBase');\n"); printer.Print(vars, "goog.require('grpc.web.AbstractClientBase');\n"); printer.Print(vars, "goog.require('grpc.web.ClientReadableStream');\n"); printer.Print(vars, "goog.require('grpc.web.RpcError');\n"); PrintClosureDependencies(&printer, file); printer.Print(vars, "\ngoog.requireType('grpc.web.ClientOptions');\n"); printer.Print("\n\n\n"); printer.Print("goog.scope(function() {\n\n"); break; case ImportStyle::COMMONJS: printer.Print(vars, "const grpc = {};\n"); printer.Print(vars, "grpc.web = require('grpc-web');\n\n"); PrintCommonJsMessagesDeps(&printer, file); break; case ImportStyle::TYPESCRIPT: break; } for (int service_index = 0; service_index < file->service_count(); ++service_index) { const ServiceDescriptor* service = file->service(service_index); vars["service_name"] = service->name(); PrintServiceConstructor(&printer, vars, false); PrintServiceConstructor(&printer, vars, true); for (int method_index = 0; method_index < service->method_count(); ++method_index) { const MethodDescriptor* method = service->method(method_index); const Descriptor* input_type = method->input_type(); const Descriptor* output_type = method->output_type(); vars["js_method_name"] = LowercaseFirstLetter(method->name()); vars["method_name"] = method->name(); vars["in"] = input_type->full_name(); vars["out"] = output_type->full_name(); vars["method_descriptor"] = "methodDescriptor_" + service->name() + "_" + method->name(); // Cross-file ref in CommonJS needs to use the module alias instead // of the global name. if (ImportStyle::COMMONJS == generator_options.import_style() && input_type->file() != file) { vars["in_type"] = ModuleAlias(input_type->file()->name()) + GetNestedMessageName(input_type); } else { vars["in_type"] = "proto." + input_type->full_name(); } if (ImportStyle::COMMONJS == generator_options.import_style() && output_type->file() != file) { vars["out_type"] = ModuleAlias(output_type->file()->name()) + GetNestedMessageName(output_type); } else { vars["out_type"] = "proto." + output_type->full_name(); } // Client streaming is not supported yet if (!method->client_streaming()) { if (method->server_streaming()) { vars["method_type"] = "grpc.web.MethodType.SERVER_STREAMING"; PrintMethodDescriptor(&printer, vars); vars["client_type"] = "Client"; PrintServerStreamingCall(&printer, vars); vars["client_type"] = "PromiseClient"; PrintServerStreamingCall(&printer, vars); } else { vars["method_type"] = "grpc.web.MethodType.UNARY"; PrintMethodDescriptor(&printer, vars); PrintUnaryCall(&printer, vars); PrintPromiseUnaryCall(&printer, vars); } } } } switch (generator_options.import_style()) { case ImportStyle::CLOSURE: printer.Print("}); // goog.scope\n\n"); break; case ImportStyle::COMMONJS: if (!vars["package"].empty()) { printer.Print(vars, "module.exports = proto.$package$;\n\n"); } else { printer.Print(vars, "module.exports = proto;\n\n"); } break; case ImportStyle::TYPESCRIPT: break; } if (generator_options.generate_dts()) { string grpcweb_dts_file_name = StripProto(file->name()) + "_grpc_web_pb.d.ts"; string proto_dts_file_name = StripProto(file->name()) + "_pb.d.ts"; std::unique_ptr grpcweb_dts_output( context->Open(grpcweb_dts_file_name)); Printer grpcweb_dts_printer(grpcweb_dts_output.get(), '$'); PrintGrpcWebDtsFile(&grpcweb_dts_printer, file); } if (generator_options.generate_closure_es6()) { string es6_file_name = StripProto(file->name()) + ".pb.grpc-web.js"; std::unique_ptr es6_output( context->Open(es6_file_name)); Printer es6_printer(es6_output.get(), '$'); PrintGrpcWebClosureES6File(&es6_printer, file); } return true; } }; } // namespace } // namespace web } // namespace grpc int main(int argc, char* argv[]) { if (argc == 2 && std::string(argv[1]) == "--version") { std::cout << argv[0] << " " << grpc::web::GRPC_WEB_VERSION << std::endl; return 0; } grpc::web::GrpcCodeGenerator generator; PluginMain(argc, argv, &generator); return 0; } ================================================ FILE: javascript/net/grpc/web/generictransportinterface.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * @fileoverview gRPC-Web generic transport interface * * This class provides an abstraction for the underlying transport * implementation underneath the ClientReadableStream layer. * * @author stanleycheung@google.com (Stanley Cheung) */ goog.module('grpc.web.GenericTransportInterface'); goog.module.declareLegacyNamespace(); const NodeReadableStream = goog.require('goog.net.streams.NodeReadableStream'); const XhrIo = goog.require('goog.net.XhrIo'); /** * @typedef {{ * nodeReadableStream: (?NodeReadableStream|undefined), * xhr: (?XhrIo|undefined), * }} */ exports.GenericTransportInterface; ================================================ FILE: javascript/net/grpc/web/grpcwebclientbase.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * @fileoverview gRPC browser client library. * * Base class for gRPC Web JS clients using the application/grpc-web wire * format * * @author stanleycheung@google.com (Stanley Cheung) */ goog.module('grpc.web.GrpcWebClientBase'); goog.module.declareLegacyNamespace(); const ClientOptions = goog.requireType('grpc.web.ClientOptions'); const ClientReadableStream = goog.require('grpc.web.ClientReadableStream'); const ClientUnaryCallImpl = goog.require('grpc.web.ClientUnaryCallImpl'); const GrpcWebClientReadableStream = goog.require('grpc.web.GrpcWebClientReadableStream'); const HttpCors = goog.require('goog.net.rpc.HttpCors'); const MethodDescriptor = goog.requireType('grpc.web.MethodDescriptor'); const Request = goog.require('grpc.web.Request'); const RpcError = goog.require('grpc.web.RpcError'); const StatusCode = goog.require('grpc.web.StatusCode'); const XhrIo = goog.require('goog.net.XhrIo'); const googCrypt = goog.require('goog.crypt.base64'); const {AbstractClientBase, PromiseCallOptions, getHostname} = goog.require('grpc.web.AbstractClientBase'); const {Status} = goog.require('grpc.web.Status'); const {StreamInterceptor, UnaryInterceptor} = goog.require('grpc.web.Interceptor'); const {toObject} = goog.require('goog.collections.maps'); /** * Base class for gRPC web client using the application/grpc-web wire format * @implements {AbstractClientBase} * @unrestricted */ class GrpcWebClientBase { /** * @param {!ClientOptions=} options * @param {!XhrIo=} xhrIo */ constructor(options = {}, xhrIo = undefined) { /** * @const * @private {string} */ this.format_ = options.format || goog.getObjectByName('format', options) || 'text'; /** * @const * @private {boolean} */ this.suppressCorsPreflight_ = options.suppressCorsPreflight || goog.getObjectByName('suppressCorsPreflight', options) || false; /** * @const * @private {boolean} */ this.withCredentials_ = options.withCredentials || goog.getObjectByName('withCredentials', options) || false; /** * @const {!Array} * @private */ this.streamInterceptors_ = options.streamInterceptors || goog.getObjectByName('streamInterceptors', options) || []; /** * @const {!Array} * @private */ this.unaryInterceptors_ = options.unaryInterceptors || goog.getObjectByName('unaryInterceptors', options) || []; /** @const @private {?XhrIo} */ this.xhrIo_ = xhrIo || null; } /** * @override * @export */ rpcCall(method, requestMessage, metadata, methodDescriptor, callback) { const hostname = getHostname(method, methodDescriptor); const invoker = GrpcWebClientBase.runInterceptors_( (request) => this.startStream_(request, hostname), this.streamInterceptors_); const stream = /** @type {!ClientReadableStream} */ (invoker.call( this, methodDescriptor.createRequest(requestMessage, metadata))); GrpcWebClientBase.setCallback_(stream, callback, false); return new ClientUnaryCallImpl(stream); } /** * @param {string} method The method to invoke * @param {REQUEST} requestMessage The request proto * @param {!Object} metadata User defined call metadata * @param {!MethodDescriptor} methodDescriptor * @param {?PromiseCallOptions=} options Options for the call * @return {!Promise} * @template REQUEST, RESPONSE */ thenableCall( method, requestMessage, metadata, methodDescriptor, options = {}) { const hostname = getHostname(method, methodDescriptor); const signal = options && options.signal; const initialInvoker = (request) => new Promise((resolve, reject) => { // If the signal is already aborted, immediately reject the promise // and don't issue the call. if (signal && signal.aborted) { const error = new RpcError(StatusCode.CANCELLED, 'Aborted'); error.cause = signal.reason; reject(error); return; } const stream = this.startStream_(request, hostname); let unaryMetadata; let unaryStatus; let unaryMsg; GrpcWebClientBase.setCallback_( stream, (error, response, status, metadata, unaryResponseReceived) => { if (error) { reject(error); } else if (unaryResponseReceived) { unaryMsg = response; } else if (status) { unaryStatus = status; } else if (metadata) { unaryMetadata = metadata; } else { resolve(request.getMethodDescriptor().createUnaryResponse( unaryMsg, unaryMetadata, unaryStatus)); } }, true); // Wire up cancellation from the abort signal, if any. if (signal) { signal.addEventListener('abort', () => { stream.cancel(); const error = new RpcError(StatusCode.CANCELLED, 'Aborted'); error.cause = /** @type {!AbortSignal} */ (signal).reason; reject(error); }); } }); const invoker = GrpcWebClientBase.runInterceptors_( initialInvoker, this.unaryInterceptors_); const unaryResponse = /** @type {!Promise} */ (invoker.call( this, methodDescriptor.createRequest(requestMessage, metadata))); return unaryResponse.then((response) => response.getResponseMessage()); } /** * @export * @param {string} method The method to invoke * @param {REQUEST} requestMessage The request proto * @param {!Object} metadata User defined call metadata * @param {!MethodDescriptor} methodDescriptor Information * of this RPC method * @param {?PromiseCallOptions=} options Options for the call * @return {!Promise} * @template REQUEST, RESPONSE */ unaryCall(method, requestMessage, metadata, methodDescriptor, options = {}) { return /** @type {!Promise}*/ (this.thenableCall( method, requestMessage, metadata, methodDescriptor, options)); } /** * @override * @export */ serverStreaming(method, requestMessage, metadata, methodDescriptor) { const hostname = getHostname(method, methodDescriptor); const invoker = GrpcWebClientBase.runInterceptors_( (request) => this.startStream_(request, hostname), this.streamInterceptors_); return /** @type {!ClientReadableStream} */ (invoker.call( this, methodDescriptor.createRequest(requestMessage, metadata))); } /** * @private * @template REQUEST, RESPONSE * @param {!Request} request * @param {string} hostname * @return {!ClientReadableStream} */ startStream_(request, hostname) { const methodDescriptor = request.getMethodDescriptor(); let path = hostname + methodDescriptor.getName(); const xhr = this.xhrIo_ ? this.xhrIo_ : new XhrIo(); xhr.setWithCredentials(this.withCredentials_); const genericTransportInterface = { xhr: xhr, }; const stream = new GrpcWebClientReadableStream(genericTransportInterface); stream.setResponseDeserializeFn( methodDescriptor.getResponseDeserializeFn()); const metadata = request.getMetadata(); for(const key in metadata) { xhr.headers.set(key, metadata[key]); } this.processHeaders_(xhr); if (this.suppressCorsPreflight_) { const headerObject = toObject(xhr.headers); xhr.headers.clear(); path = GrpcWebClientBase.setCorsOverride_(path, headerObject); } const requestSerializeFn = methodDescriptor.getRequestSerializeFn(); const serialized = requestSerializeFn(request.getRequestMessage()); let payload = this.encodeRequest_(serialized); if (this.format_ == 'text') { payload = googCrypt.encodeByteArray(payload); } else if (this.format_ == 'binary') { xhr.setResponseType(XhrIo.ResponseType.ARRAY_BUFFER); } xhr.send(path, 'POST', payload); return stream; } /** * @private * @static * @template RESPONSE * @param {!ClientReadableStream} stream * @param {function(?RpcError, ?RESPONSE, ?Status=, ?Object=, ?boolean)| * function(?RpcError,?RESPONSE)} callback * @param {boolean} useUnaryResponse Pass true to have the client make * multiple calls to the callback, using (error, response, status, * metadata, unaryResponseReceived) arguments. One of error, status, * metadata, or unaryResponseReceived will be truthy to indicate which piece * of information the client is providing in that call. After the stream * ends, it will call the callback an additional time with all falsy * arguments. Pass false to have the client make one call to the callback * using (error, response) arguments. */ static setCallback_(stream, callback, useUnaryResponse) { let isResponseReceived = false; let responseReceived = null; let errorEmitted = false; stream.on('data', function(response) { isResponseReceived = true; responseReceived = response; }); stream.on('error', function(error) { if (error.code != StatusCode.OK && !errorEmitted) { errorEmitted = true; callback(error, null); } }); stream.on('status', function(status) { if (status.code != StatusCode.OK && !errorEmitted) { errorEmitted = true; callback( { code: status.code, message: status.details, metadata: status.metadata }, null); } else if (useUnaryResponse) { callback(null, null, status); } }); if (useUnaryResponse) { stream.on('metadata', function(metadata) { callback(null, null, null, metadata); }); } stream.on('end', function() { if (!errorEmitted) { if (!isResponseReceived) { callback({ code: StatusCode.UNKNOWN, message: 'Incomplete response', }); } else if (useUnaryResponse) { callback( null, responseReceived, null, null, /* unaryResponseReceived= */ true); } else { callback(null, responseReceived); } } if (useUnaryResponse) { callback(null, null); } }); } /** * Encode the grpc-web request * * @private * @param {!Uint8Array} serialized The serialized proto payload * @return {!Uint8Array} The application/grpc-web padded request */ encodeRequest_(serialized) { let len = serialized.length; const bytesArray = [0, 0, 0, 0]; const payload = new Uint8Array(5 + len); for (let i = 3; i >= 0; i--) { bytesArray[i] = (len % 256); len = len >>> 8; } payload.set(new Uint8Array(bytesArray), 1); payload.set(serialized, 5); return payload; } /** * @private * @param {!XhrIo} xhr The xhr object */ processHeaders_(xhr) { if (this.format_ == 'text') { xhr.headers.set('Content-Type', 'application/grpc-web-text'); xhr.headers.set('Accept', 'application/grpc-web-text'); } else { xhr.headers.set('Content-Type', 'application/grpc-web+proto'); } xhr.headers.set('X-User-Agent', 'grpc-web-javascript/0.1'); xhr.headers.set('X-Grpc-Web', '1'); if (xhr.headers.has('deadline')) { const deadline = Number(xhr.headers.get('deadline')); // in ms const currentTime = (new Date()).getTime(); let timeout = Math.ceil(deadline - currentTime); xhr.headers.delete('deadline'); if (timeout === Infinity) { // grpc-timeout header defaults to infinity if not set. timeout = 0; } if (timeout > 0) { xhr.headers.set('grpc-timeout', timeout + 'm'); // Also set timeout on the xhr request to terminate the HTTP request // if the server doesn't respond within the deadline. We use 110% of // grpc-timeout for this to allow the server to terminate the connection // with DEADLINE_EXCEEDED rather than terminating it in the Browser, but // at least 1 second in case the user is on a high-latency network. xhr.setTimeoutInterval(Math.max(1000, Math.ceil(timeout * 1.1))); } } } /** * @private * @static * @param {string} method The method to invoke * @param {!Object} headerObject The xhr headers * @return {string} The URI object or a string path with headers */ static setCorsOverride_(method, headerObject) { return /** @type {string} */ (HttpCors.setHttpHeadersWithOverwriteParam( method, HttpCors.HTTP_HEADERS_PARAM_NAME, headerObject)); } /** * @private * @static * @template REQUEST, RESPONSE * @param {function(!Request): * (!Promise|!ClientReadableStream)} invoker * @param {!Array} * interceptors * @return {function(!Request): * (!Promise|!ClientReadableStream)} */ static runInterceptors_(invoker, interceptors) { return interceptors.reduce((accumulatedInvoker, interceptor) => { return (request) => interceptor.intercept(request, accumulatedInvoker); }, invoker); } } exports = GrpcWebClientBase; ================================================ FILE: javascript/net/grpc/web/grpcwebclientbase_test.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ goog.module('grpc.web.GrpcWebClientBaseTest'); goog.setTestOnly('grpc.web.GrpcWebClientBaseTest'); const ClientReadableStream = goog.require('grpc.web.ClientReadableStream'); const ErrorCode = goog.require('goog.net.ErrorCode'); const GrpcWebClientBase = goog.require('grpc.web.GrpcWebClientBase'); const MethodDescriptor = goog.require('grpc.web.MethodDescriptor'); const ReadyState = goog.require('goog.net.XmlHttp.ReadyState'); const Request = goog.requireType('grpc.web.Request'); const RpcError = goog.require('grpc.web.RpcError'); const StatusCode = goog.require('grpc.web.StatusCode'); const XhrIo = goog.require('goog.testing.net.XhrIo'); const googCrypt = goog.require('goog.crypt.base64'); const testSuite = goog.require('goog.testing.testSuite'); const {StreamInterceptor} = goog.require('grpc.web.Interceptor'); goog.require('goog.testing.jsunit'); // This parses to [ { DATA: [4, 5, 6] }, { TRAILER: "a: b" } ] const DEFAULT_RPC_RESPONSE = new Uint8Array([0, 0, 0, 0, 3, 4, 5, 6, 128, 0, 0, 0, 4, 97, 58, 32, 98]); const DEFAULT_RPC_RESPONSE_DATA = [4, 5, 6]; const DEFAULT_UNARY_HEADERS = ['Content-Type', 'Accept', 'X-User-Agent', 'X-Grpc-Web']; const DEFAULT_UNARY_HEADER_VALUES = [ 'application/grpc-web-text', 'application/grpc-web-text', 'grpc-web-javascript/0.1', '1', ]; const DEFAULT_RESPONSE_HEADERS = { 'Content-Type': 'application/grpc-web-text', }; testSuite({ async testRpcResponse() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); const methodDescriptor = createMethodDescriptor((bytes) => { assertElementsEquals(DEFAULT_RPC_RESPONSE_DATA, [].slice.call(bytes)); return new MockReply('value'); }); const response = await new Promise((resolve, reject) => { client.rpcCall( 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor, (error, response) => { assertNull(error); resolve(response); }); xhr.simulatePartialResponse( googCrypt.encodeByteArray(new Uint8Array(DEFAULT_RPC_RESPONSE)), DEFAULT_RESPONSE_HEADERS); xhr.simulateReadyStateChange(ReadyState.COMPLETE); }); assertEquals('value', response.data); const headers = /** @type {!Object} */ (xhr.getLastRequestHeaders()); assertElementsEquals(DEFAULT_UNARY_HEADERS, Object.keys(headers)); assertElementsEquals(DEFAULT_UNARY_HEADER_VALUES, Object.values(headers)); }, async testRpcFalsyResponse_ForNonProtobufDescriptor() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); const methodDescriptor = createMethodDescriptor((bytes) => { assertElementsEquals(DEFAULT_RPC_RESPONSE_DATA, [].slice.call(bytes)); return 0; }); const response = await new Promise((resolve, reject) => { client.rpcCall( 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor, (error, response) => { assertNull(error); resolve(response); }); xhr.simulatePartialResponse( googCrypt.encodeByteArray(new Uint8Array(DEFAULT_RPC_RESPONSE)), DEFAULT_RESPONSE_HEADERS); xhr.simulateReadyStateChange(ReadyState.COMPLETE); }); assertEquals(0, response); const headers = /** @type {!Object} */ (xhr.getLastRequestHeaders()); assertElementsEquals(DEFAULT_UNARY_HEADERS, Object.keys(headers)); assertElementsEquals(DEFAULT_UNARY_HEADER_VALUES, Object.values(headers)); }, async testRpcResponseThenableCall() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); const methodDescriptor = createMethodDescriptor((bytes) => { assertElementsEquals(DEFAULT_RPC_RESPONSE_DATA, [].slice.call(bytes)); return new MockReply('value'); }); const responsePromise = client.thenableCall( 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor); xhr.simulatePartialResponse( googCrypt.encodeByteArray(new Uint8Array(DEFAULT_RPC_RESPONSE)), DEFAULT_RESPONSE_HEADERS); xhr.simulateReadyStateChange(ReadyState.COMPLETE); const response = await responsePromise; assertEquals('value', /** @type {!MockReply} */ (response).data); const headers = /** @type {!Object} */ (xhr.getLastRequestHeaders()); assertElementsEquals(DEFAULT_UNARY_HEADERS, Object.keys(headers)); assertElementsEquals(DEFAULT_UNARY_HEADER_VALUES, Object.values(headers)); }, async testRpcFalsyResponseThenableCall_ForNonProtobufDescriptor() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); const methodDescriptor = createMethodDescriptor((bytes) => { assertElementsEquals(DEFAULT_RPC_RESPONSE_DATA, [].slice.call(bytes)); return 0; }); const responsePromise = client.thenableCall( 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor); xhr.simulatePartialResponse( googCrypt.encodeByteArray(new Uint8Array(DEFAULT_RPC_RESPONSE)), DEFAULT_RESPONSE_HEADERS); xhr.simulateReadyStateChange(ReadyState.COMPLETE); const response = await responsePromise; assertEquals(0, response); const headers = /** @type {!Object} */ (xhr.getLastRequestHeaders()); assertElementsEquals(DEFAULT_UNARY_HEADERS, Object.keys(headers)); assertElementsEquals(DEFAULT_UNARY_HEADER_VALUES, Object.values(headers)); }, async testCancelledThenableCall() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); const methodDescriptor = createMethodDescriptor((bytes) => { assertElementsEquals(DEFAULT_RPC_RESPONSE_DATA, [].slice.call(bytes)); return 0; }); const abortController = new AbortController(); const signal = abortController.signal; const responsePromise = client.thenableCall( 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor, {signal}); abortController.abort(); const error = await assertRejects(responsePromise); assertTrue(error instanceof RpcError); assertEquals(StatusCode.CANCELLED, /** @type {!RpcError} */ (error).code); assertEquals('Aborted', /** @type {!RpcError} */ (error).message); // Default abort reason if none provided. const cause = /** @type {!RpcError} */ (error).cause; assertTrue(cause instanceof Error); assertEquals('AbortError', /** @type {!Error} */ (cause).name); assertEquals(ErrorCode.ABORT, xhr.getLastErrorCode()); }, async testCancelledThenableCallWithReason() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); const methodDescriptor = createMethodDescriptor((bytes) => { assertElementsEquals(DEFAULT_RPC_RESPONSE_DATA, [].slice.call(bytes)); return 0; }); const abortController = new AbortController(); const signal = abortController.signal; const responsePromise = client.thenableCall( 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor, {signal}); abortController.abort('cancelling'); const error = await assertRejects(responsePromise); assertTrue(error instanceof RpcError); assertEquals(StatusCode.CANCELLED, /** @type {!RpcError} */ (error).code); assertEquals('Aborted', /** @type {!RpcError} */ (error).message); // Abort reason forwarded as cause. const cause = /** @type {!RpcError} */ (error).cause; assertEquals('cancelling', cause); assertEquals(ErrorCode.ABORT, xhr.getLastErrorCode()); }, async testDeadline() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); const methodDescriptor = createMethodDescriptor((bytes) => new MockReply()); const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 1); await new Promise((resolve, reject) => { client.rpcCall( 'url', new MockRequest(), {'deadline': deadline.getTime().toString()}, methodDescriptor, (error, response) => { assertNull(error); resolve(); }); xhr.simulatePartialResponse( googCrypt.encodeByteArray(new Uint8Array(DEFAULT_RPC_RESPONSE)), DEFAULT_RESPONSE_HEADERS); xhr.simulateReadyStateChange(ReadyState.COMPLETE); }); const headers = /** @type {!Object} */ (xhr.getLastRequestHeaders()); const headersWithDeadline = [...DEFAULT_UNARY_HEADERS, 'grpc-timeout']; assertElementsEquals(headersWithDeadline, Object.keys(headers)); }, async testRpcError() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); const methodDescriptor = createMethodDescriptor((bytes) => new MockReply()); const error = await new Promise((resolve, reject) => { client.rpcCall( 'urlurl', new MockRequest(), /* metadata= */ {}, methodDescriptor, (error, response) => { assertNull(response); resolve(error); }); // This decodes to "grpc-status: 3" xhr.simulatePartialResponse( googCrypt.encodeByteArray(new Uint8Array([ 128, 0, 0, 0, 14, 103, 114, 112, 99, 45, 115, 116, 97, 116, 117, 115, 58, 32, 51, ])), DEFAULT_RESPONSE_HEADERS); }); assertTrue(error instanceof RpcError); assertEquals(3, error.code); }, async testRpcDeserializationError() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); const responseDeserializeFn = () => { throw new Error('Decoding error :)'); }; const methodDescriptor = createMethodDescriptor(responseDeserializeFn); const error = await new Promise((resolve, reject) => { client.rpcCall( 'urlurl', new MockRequest(), /* metadata= */ {}, methodDescriptor, (error, response) => { assertNull(response); resolve(error); }); xhr.simulatePartialResponse( googCrypt.encodeByteArray(new Uint8Array(DEFAULT_RPC_RESPONSE)), DEFAULT_RESPONSE_HEADERS); }); assertTrue(error instanceof RpcError); assertEquals(StatusCode.INTERNAL, error.code); }, async testRpcResponseHeader() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); const methodDescriptor = createMethodDescriptor((bytes) => { assertElementsEquals(DEFAULT_RPC_RESPONSE_DATA, [].slice.call(bytes)); return new MockReply('value'); }); const metadata = await new Promise((resolve, reject) => { const call = client.rpcCall( 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor, (error, response) => { assertNull(error); }); call.on('metadata', (metadata) => { resolve(metadata); }); xhr.simulatePartialResponse( googCrypt.encodeByteArray(new Uint8Array(DEFAULT_RPC_RESPONSE)), { 'Content-Type': 'application/grpc-web-text', 'initial-metadata-key': 'initial-metadata-value', }); xhr.simulateReadyStateChange(ReadyState.COMPLETE); }); assertEquals('initial-metadata-value', metadata['initial-metadata-key']); }, async testStreamInterceptor() { const xhr = new XhrIo(); const interceptor = new StreamResponseInterceptor(); const methodDescriptor = createMethodDescriptor((bytes) => { assertElementsEquals(DEFAULT_RPC_RESPONSE_DATA, [].slice.call(bytes)); return new MockReply('value'); }); const client = new GrpcWebClientBase({'streamInterceptors': [interceptor]}, xhr); const response = await new Promise((resolve, reject) => { client.rpcCall( 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor, (error, response) => { assertNull(error); resolve(response); }); xhr.simulatePartialResponse( googCrypt.encodeByteArray(new Uint8Array(DEFAULT_RPC_RESPONSE)), DEFAULT_RESPONSE_HEADERS); xhr.simulateReadyStateChange(ReadyState.COMPLETE); }); assertEquals('Intercepted value', response.data); }, }); /** Mocks a request proto object. */ class MockRequest { /** * @param {string=} data */ constructor(data = '') { /** @type {string} */ this.data = data; } } /** Mocks a response proto object. */ class MockReply { /** * @param {string=} data */ constructor(data = '') { /** @type {string} */ this.data = data; } } /** * Typedef for allowed response types. * * Number is allowed specifically for supporting falsy responses `0`, see: * https://github.com/grpc/grpc-web/pull/1025 * * @typedef {!MockReply|number} */ let AllowedResponseType; /** * @param {function(string): !AllowedResponseType} responseDeSerializeFn * @return {!MethodDescriptor} */ function createMethodDescriptor(responseDeSerializeFn) { return new MethodDescriptor( /* name= */ '', /* methodType= */ null, MockRequest, MockReply, (request) => [1, 2, 3], responseDeSerializeFn); } /** * @implements {StreamInterceptor} * @unrestricted */ class StreamResponseInterceptor { constructor() {} /** * @override * @template REQUEST, RESPONSE * @param {!Request} request * @param {function(!Request): * !ClientReadableStream} invoker * @return {!ClientReadableStream} */ intercept(request, invoker) { return new InterceptedStream(invoker(request)); } } /** * @implements {ClientReadableStream} * @template RESPONSE * @final */ class InterceptedStream { /** * @param {!ClientReadableStream} stream */ constructor(stream) { /** @const {!ClientReadableStream} */ this.stream = stream; } /** * @override * @param {string} eventType * @param {function(?)} callback * @return {!ClientReadableStream} */ on(eventType, callback) { if (eventType == 'data') { const newCallback = (response) => { response.data = 'Intercepted ' + response.data; callback(response); }; this.stream.on(eventType, newCallback); } else { this.stream.on(eventType, callback); } return this; } /** * @override * @return {!ClientReadableStream} */ cancel() { this.stream.cancel(); return this; } /** * @override * @param {string} eventType * @param {function(?)} callback * @return {!ClientReadableStream} */ removeListener(eventType, callback) { this.stream.removeListener(eventType, callback); return this; } } ================================================ FILE: javascript/net/grpc/web/grpcwebclientreadablestream.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * @fileoverview gRPC web client Readable Stream * * This class is being returned after a gRPC streaming call has been * started. This class provides functionality for user to operates on * the stream, e.g. set onData callback, etc. * * This wraps the underlying goog.net.streams.NodeReadableStream * * @author stanleycheung@google.com (Stanley Cheung) */ goog.module('grpc.web.GrpcWebClientReadableStream'); goog.module.declareLegacyNamespace(); const ClientReadableStream = goog.require('grpc.web.ClientReadableStream'); const ErrorCode = goog.require('goog.net.ErrorCode'); const EventType = goog.require('goog.net.EventType'); const GrpcWebStreamParser = goog.require('grpc.web.GrpcWebStreamParser'); const RpcError = goog.require('grpc.web.RpcError'); const StatusCode = goog.require('grpc.web.StatusCode'); const XhrIo = goog.require('goog.net.XhrIo'); const events = goog.require('goog.events'); const googCrypt = goog.require('goog.crypt.base64'); const googString = goog.require('goog.string'); const {GenericTransportInterface} = goog.require('grpc.web.GenericTransportInterface'); const {Status} = goog.require('grpc.web.Status'); const GRPC_STATUS = 'grpc-status'; const GRPC_STATUS_MESSAGE = 'grpc-message'; /** @type {!Array} */ const EXCLUDED_RESPONSE_HEADERS = ['content-type', GRPC_STATUS, GRPC_STATUS_MESSAGE]; /** * A stream that the client can read from. Used for calls that are streaming * from the server side. * @template RESPONSE * @implements {ClientReadableStream} * @final * @unrestricted */ class GrpcWebClientReadableStream { /** * @param {!GenericTransportInterface} genericTransportInterface The * GenericTransportInterface */ constructor(genericTransportInterface) { /** * @const * @private * @type {?XhrIo} The XhrIo object */ this.xhr_ = /** @type {?XhrIo} */ (genericTransportInterface.xhr); /** * @private * @type {function(?):!RESPONSE|null} The deserialize function for the proto */ this.responseDeserializeFn_ = null; /** * @const * @private * @type {!Array} The list of data callbacks */ this.onDataCallbacks_ = []; /** * @const * @private * @type {!Array} The list of status callbacks */ this.onStatusCallbacks_ = []; /** * @const * @private * @type {!Array} The list of metadata callbacks */ this.onMetadataCallbacks_ = []; /** * @const * @private * @type {!Array} The list of error callbacks */ this.onErrorCallbacks_ = []; /** * @const * @private * @type {!Array} The list of stream end callbacks */ this.onEndCallbacks_ = []; /** * @private * @type {boolean} Whether the stream has been aborted */ this.aborted_ = false; /** * @private * @type {number} The stream parser position */ this.pos_ = 0; /** * @private * @type {!GrpcWebStreamParser} The grpc-web stream parser * @const */ this.parser_ = new GrpcWebStreamParser(); const self = this; events.listen(this.xhr_, EventType.READY_STATE_CHANGE, function(e) { let contentType = self.xhr_.getStreamingResponseHeader('Content-Type'); if (!contentType) return; contentType = contentType.toLowerCase(); let byteSource; if (googString.startsWith(contentType, 'application/grpc-web-text')) { // Ensure responseText is not null const responseText = self.xhr_.getResponseText() || ''; const newPos = responseText.length - responseText.length % 4; const newData = responseText.substr(self.pos_, newPos - self.pos_); if (newData.length == 0) return; self.pos_ = newPos; byteSource = googCrypt.decodeStringToUint8Array(newData); } else if (googString.startsWith(contentType, 'application/grpc')) { byteSource = new Uint8Array( /** @type {!ArrayBuffer} */ (self.xhr_.getResponse())); } else { self.handleError_( new RpcError(StatusCode.UNKNOWN, 'Unknown Content-type received.')); return; } let messages = null; try { messages = self.parser_.parse(byteSource); } catch (err) { self.handleError_( new RpcError(StatusCode.UNKNOWN, 'Error in parsing response body')); } if (messages) { const FrameType = GrpcWebStreamParser.FrameType; for (let i = 0; i < messages.length; i++) { if (FrameType.DATA in messages[i]) { const data = messages[i][FrameType.DATA]; if (data) { let isResponseDeserialized = false; let response; try { response = self.responseDeserializeFn_(data); isResponseDeserialized = true; } catch (err) { self.handleError_(new RpcError( StatusCode.INTERNAL, `Error when deserializing response data; error: ${err}` + `, response: ${response}`)); } if (isResponseDeserialized) { self.sendDataCallbacks_(response); } } } if (FrameType.TRAILER in messages[i]) { if (messages[i][FrameType.TRAILER].length > 0) { let trailerString = ''; for (let pos = 0; pos < messages[i][FrameType.TRAILER].length; pos++) { trailerString += String.fromCharCode(messages[i][FrameType.TRAILER][pos]); } const trailers = self.parseHttp1Headers_(trailerString); let grpcStatusCode = StatusCode.OK; let grpcStatusMessage = ''; if (GRPC_STATUS in trailers) { grpcStatusCode = /** @type {!StatusCode} */ (Number(trailers[GRPC_STATUS])); delete trailers[GRPC_STATUS]; } if (GRPC_STATUS_MESSAGE in trailers) { grpcStatusMessage = trailers[GRPC_STATUS_MESSAGE]; delete trailers[GRPC_STATUS_MESSAGE]; } self.handleError_( new RpcError(grpcStatusCode, grpcStatusMessage, trailers)); } } } } }); events.listen(this.xhr_, EventType.COMPLETE, function(e) { const lastErrorCode = self.xhr_.getLastErrorCode(); let grpcStatusCode = StatusCode.UNKNOWN; let grpcStatusMessage = ''; const initialMetadata = /** @type {!Metadata} */ ({}); // Get response headers with lower case keys. const rawResponseHeaders = self.xhr_.getResponseHeaders(); const responseHeaders = {}; for (const key in rawResponseHeaders) { if (rawResponseHeaders.hasOwnProperty(key)) { responseHeaders[key.toLowerCase()] = rawResponseHeaders[key]; } } Object.keys(responseHeaders).forEach((header_) => { if (!(EXCLUDED_RESPONSE_HEADERS.includes(header_))) { initialMetadata[header_] = responseHeaders[header_]; } }); self.sendMetadataCallbacks_(initialMetadata); // There's an XHR level error let xhrStatusCode = -1; if (lastErrorCode != ErrorCode.NO_ERROR) { switch (lastErrorCode) { case ErrorCode.ABORT: grpcStatusCode = StatusCode.ABORTED; break; case ErrorCode.TIMEOUT: grpcStatusCode = StatusCode.DEADLINE_EXCEEDED; break; case ErrorCode.HTTP_ERROR: xhrStatusCode = self.xhr_.getStatus(); grpcStatusCode = StatusCode.fromHttpStatus(xhrStatusCode); break; default: grpcStatusCode = StatusCode.UNAVAILABLE; } if (grpcStatusCode == StatusCode.ABORTED && self.aborted_) { return; } let errorMessage = ErrorCode.getDebugMessage(lastErrorCode); if (xhrStatusCode != -1) { errorMessage += ', http status code: ' + xhrStatusCode; } self.handleError_(new RpcError(grpcStatusCode, errorMessage)); return; } let errorEmitted = false; // Check whethere there are grpc specific response headers if (GRPC_STATUS in responseHeaders) { grpcStatusCode = /** @type {!StatusCode} */ ( Number(responseHeaders[GRPC_STATUS])); if (GRPC_STATUS_MESSAGE in responseHeaders) { grpcStatusMessage = responseHeaders[GRPC_STATUS_MESSAGE]; } if (grpcStatusCode != StatusCode.OK) { self.handleError_(new RpcError( grpcStatusCode, grpcStatusMessage || '', responseHeaders)); errorEmitted = true; } } if (!errorEmitted) { self.sendEndCallbacks_(); } }); } /** * @override * @export */ on(eventType, callback) { // TODO(stanleycheung): change eventType to @enum type if (eventType == 'data') { this.onDataCallbacks_.push(callback); } else if (eventType == 'status') { this.onStatusCallbacks_.push(callback); } else if (eventType == 'metadata') { this.onMetadataCallbacks_.push(callback); } else if (eventType == 'end') { this.onEndCallbacks_.push(callback); } else if (eventType == 'error') { this.onErrorCallbacks_.push(callback); } return this; } /** * @private * @param {!Array} callbacks the internal list of callbacks * @param {function(?)} callback the callback to remove */ removeListenerFromCallbacks_(callbacks, callback) { const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } } /** * @export * @override */ removeListener(eventType, callback) { if (eventType == 'data') { this.removeListenerFromCallbacks_(this.onDataCallbacks_, callback); } else if (eventType == 'status') { this.removeListenerFromCallbacks_(this.onStatusCallbacks_, callback); } else if (eventType == 'metadata') { this.removeListenerFromCallbacks_(this.onMetadataCallbacks_, callback); } else if (eventType == 'end') { this.removeListenerFromCallbacks_(this.onEndCallbacks_, callback); } else if (eventType == 'error') { this.removeListenerFromCallbacks_(this.onErrorCallbacks_, callback); } return this; } /** * Register a callbackl to parse the response * * @param {function(?):!RESPONSE} responseDeserializeFn The deserialize * function for the proto */ setResponseDeserializeFn(responseDeserializeFn) { this.responseDeserializeFn_ = responseDeserializeFn; } /** * @override * @export */ cancel() { this.aborted_ = true; this.xhr_.abort(); } /** * Parse HTTP headers * * @private * @param {string} str The raw http header string * @return {!Object} The header:value pairs */ parseHttp1Headers_(str) { const chunks = str.trim().split('\r\n'); const headers = {}; for (let i = 0; i < chunks.length; i++) { const pos = chunks[i].indexOf(':'); headers[chunks[i].substring(0, pos).trim()] = chunks[i].substring(pos + 1).trim(); } return headers; } /** * A central place to handle errors * * @private * @param {!RpcError} error The error object */ handleError_(error) { if (error.code != StatusCode.OK) { this.sendErrorCallbacks_(new RpcError( error.code, decodeURIComponent(error.message || ''), error.metadata)); } this.sendStatusCallbacks_(/** @type {!Status} */ ({ code: error.code, details: decodeURIComponent(error.message || ''), metadata: error.metadata })); } /** * @private * @param {!RESPONSE} data The data to send back */ sendDataCallbacks_(data) { for (let i = 0; i < this.onDataCallbacks_.length; i++) { this.onDataCallbacks_[i](data); } } /** * @private * @param {!Status} status The status to send back */ sendStatusCallbacks_(status) { for (let i = 0; i < this.onStatusCallbacks_.length; i++) { this.onStatusCallbacks_[i](status); } } /** * @private * @param {!Metadata} metadata The metadata to send back */ sendMetadataCallbacks_(metadata) { for (let i = 0; i < this.onMetadataCallbacks_.length; i++) { this.onMetadataCallbacks_[i](metadata); } } /** * @private * @param {!RpcError} error The error to send back */ sendErrorCallbacks_(error) { for (let i = 0; i < this.onErrorCallbacks_.length; i++) { this.onErrorCallbacks_[i](error); } } /** * @private */ sendEndCallbacks_() { for (let i = 0; i < this.onEndCallbacks_.length; i++) { this.onEndCallbacks_[i](); } } } exports = GrpcWebClientReadableStream; ================================================ FILE: javascript/net/grpc/web/grpcwebstreamparser.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * @fileoverview The default grpc-web stream parser * * The default grpc-web parser decodes the input stream (binary) under the * following rules: * * 1. The wire format looks like: * * 0x00 0x80 * * For details of grpc-web wire format see * https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md * * 2. Messages will be delivered once each frame is completed. Partial stream * segments are accepted. * * 3. Example: * * Incoming data: 0x00 0x00 0x80 * * Result: [ { 0x00 : }, { 0x80 : trailers } ] */ goog.module('grpc.web.GrpcWebStreamParser'); goog.module.declareLegacyNamespace(); const StreamParser = goog.require('goog.net.streams.StreamParser'); const asserts = goog.require('goog.asserts'); /** * The default grpc-web stream parser. * @implements {StreamParser} * @final */ class GrpcWebStreamParser { constructor() { /** * The current error message, if any. * @private {?string} */ this.errorMessage_ = null; /** * The currently buffered result (parsed messages). * @private {!Array} */ this.result_ = []; /** * The current position in the streamed data. * @private {number} */ this.streamPos_ = 0; /** * The current parser state. * @private {number} */ this.state_ = Parser.State_.INIT; /** * The current frame byte being parsed * @private {number} */ this.frame_ = 0; /** * The length of the proto message being parsed. * @private {number} */ this.length_ = 0; /** * Count of processed length bytes. * @private {number} */ this.countLengthBytes_ = 0; /** * Raw bytes of the current message. Uses Uint8Array by default. Falls back * to native array when Uint8Array is unsupported. * @private {?Uint8Array|?Array} */ this.messageBuffer_ = null; /** * Count of processed message bytes. * @private {number} */ this.countMessageBytes_ = 0; } /** * @override */ isInputValid() { return this.state_ != Parser.State_.INVALID; } /** * @override */ getErrorMessage() { return this.errorMessage_; } /** * @override * @return {boolean} */ acceptsBinaryInput() { return true; } /** * Parse the new input. * * Note that there is no Parser state to indicate the end of a stream. * * @param {string|!ArrayBuffer|!Uint8Array|!Array} input The input * data * @throws {!Error} Throws an error message if the input is invalid. * @return {?Array} any parsed objects (atomic messages) * in an array, or null if more data needs be read to parse any new object. * @override */ parse(input) { asserts.assert( input instanceof Array || input instanceof ArrayBuffer || input instanceof Uint8Array); var parser = this; var inputBytes; var pos = 0; if (input instanceof Uint8Array || input instanceof Array) { inputBytes = input; } else { inputBytes = new Uint8Array(input); } while (pos < inputBytes.length) { switch (parser.state_) { case Parser.State_.INVALID: { parser.error_(inputBytes, pos, 'stream already broken'); break; } case Parser.State_.INIT: { processFrameByte(inputBytes[pos]); break; } case Parser.State_.LENGTH: { processLengthByte(inputBytes[pos]); break; } case Parser.State_.MESSAGE: { processMessageByte(inputBytes[pos]); break; } default: { throw new Error('unexpected parser state: ' + parser.state_); } } parser.streamPos_++; pos++; } var msgs = parser.result_; parser.result_ = []; return msgs.length > 0 ? msgs : null; /** * @param {number} b A frame byte to process */ function processFrameByte(b) { if (b == FrameType.DATA) { parser.frame_ = b; } else if (b == FrameType.TRAILER) { parser.frame_ = b; } else { parser.error_(inputBytes, pos, 'invalid frame byte'); } parser.state_ = Parser.State_.LENGTH; parser.length_ = 0; parser.countLengthBytes_ = 0; } /** * @param {number} b A length byte to process */ function processLengthByte(b) { parser.countLengthBytes_++; parser.length_ = (parser.length_ << 8) + b; if (parser.countLengthBytes_ == 4) { // no more length byte parser.state_ = Parser.State_.MESSAGE; parser.countMessageBytes_ = 0; if (typeof Uint8Array !== 'undefined') { parser.messageBuffer_ = new Uint8Array(parser.length_); } else { parser.messageBuffer_ = new Array(parser.length_); } if (parser.length_ == 0) { // empty message finishMessage(); } } } /** * @param {number} b A message byte to process */ function processMessageByte(b) { parser.messageBuffer_[parser.countMessageBytes_++] = b; if (parser.countMessageBytes_ == parser.length_) { finishMessage(); } } /** * Finishes up building the current message and resets parser state */ function finishMessage() { var message = {}; message[parser.frame_] = parser.messageBuffer_; parser.result_.push(message); parser.state_ = Parser.State_.INIT; } } } const Parser = GrpcWebStreamParser; /** * The parser state. * @private @enum {number} */ Parser.State_ = { INIT: 0, // expecting the next frame byte LENGTH: 1, // expecting 4 bytes of length MESSAGE: 2, // expecting more message bytes INVALID: 3 }; /** * Possible frame byte * @enum {number} */ GrpcWebStreamParser.FrameType = { DATA: 0x00, // expecting a data frame TRAILER: 0x80, // expecting a trailer frame }; var FrameType = GrpcWebStreamParser.FrameType; /** * @param {!Uint8Array|!Array} inputBytes The current input buffer * @param {number} pos The position in the current input that triggers the error * @param {string} errorMsg Additional error message * @throws {!Error} Throws an error indicating where the stream is broken * @private */ Parser.prototype.error_ = function(inputBytes, pos, errorMsg) { this.state_ = Parser.State_.INVALID; this.errorMessage_ = 'The stream is broken @' + this.streamPos_ + '/' + pos + '. ' + 'Error: ' + errorMsg + '. ' + 'With input:\n' + inputBytes; throw new Error(this.errorMessage_); }; exports = GrpcWebStreamParser; ================================================ FILE: javascript/net/grpc/web/grpcwebstreamparser_test.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ goog.module('grpc.web.GrpcWebStreamParserTest'); goog.setTestOnly('grpc.web.GrpcWebStreamParserTest'); var GrpcWebStreamParser = goog.require('grpc.web.GrpcWebStreamParser'); var testSuite = goog.require('goog.testing.testSuite'); goog.require('goog.testing.jsunit'); var parser; var FrameType = GrpcWebStreamParser.FrameType; testSuite({ setUp: function() { parser = new GrpcWebStreamParser(); }, tearDown: function() { }, testInvalidTagError: function() { var arr = new Uint8Array([1, 0]); assertThrows(function() { parser.parse(arr.buffer); }); }, testInvalidInputTypeInt: function() { assertThrows(function() { parser.parse(0); }); }, testInvalidInputTypeObject: function() { assertThrows(function() { parser.parse({}); }); }, testInvalidInputTypeString: function() { assertThrows(function() { parser.parse("abc"); }); }, testBasicMessage: function() { var arr = new Uint8Array([0, 0, 0, 0, 2, 38, 39]); var messages = parser.parse(arr.buffer); assertEquals(1, messages.length); var message = messages[0]; assertTrue(FrameType.DATA in message); assertElementsEquals([38, 39], message[FrameType.DATA]); }, testOneMessageOneTrailer: function() { var arr = new Uint8Array([0, 0, 0, 0, 2, 38, 39, 128, 0, 0, 0, 2, 40, 41]); var messages = parser.parse(arr.buffer); assertEquals(2, messages.length); var message = messages[0]; assertTrue(FrameType.DATA in message); assertElementsEquals([38, 39], message[FrameType.DATA]); var trailer = messages[1]; assertTrue(FrameType.TRAILER in trailer); assertElementsEquals([40, 41], trailer[FrameType.TRAILER]); }, testMultipleMessageOneTrailer: function() { var arr = new Uint8Array([0, 0, 0, 0, 2, 38, 39, 0, 0, 0, 0, 3, 42, 43, 44, 128, 0, 0, 0, 2, 40, 41]); var messages = parser.parse(arr.buffer); assertEquals(3, messages.length); var message1 = messages[0]; assertTrue(FrameType.DATA in message1); assertElementsEquals([38, 39], message1[FrameType.DATA]); var message2 = messages[1]; assertTrue(FrameType.DATA in message2); assertElementsEquals([42, 43, 44], message2[FrameType.DATA]); var trailer = messages[2]; assertTrue(FrameType.TRAILER in trailer); assertElementsEquals([40, 41], trailer[FrameType.TRAILER]); }, testPartialMessage: function() { var arr = new Uint8Array([0, 0, 0, 0, 2, 38]); var messages = parser.parse(arr.buffer); assertNull(messages); arr = new Uint8Array([39]); messages = parser.parse(arr.buffer); assertEquals(1, messages.length); var message = messages[0]; assertTrue(FrameType.DATA in message); assertElementsEquals([38, 39], message[FrameType.DATA]); }, testMultiplePartialMessages: function() { var arr = new Uint8Array([0, 0, 0, 0, 2, 38]); var messages = parser.parse(arr.buffer); assertNull(messages); arr = new Uint8Array([39, 128, 0, 0]); messages = parser.parse(arr.buffer); assertEquals(1, messages.length); var message = messages[0]; assertTrue(FrameType.DATA in message); assertElementsEquals([38, 39], message[FrameType.DATA]); arr = new Uint8Array([0, 3, 40]); messages = parser.parse(arr.buffer); assertNull(messages); arr = new Uint8Array([41, 42]); messages = parser.parse(arr.buffer); assertEquals(1, messages.length); var trailer = messages[0]; assertTrue(FrameType.TRAILER in trailer); assertElementsEquals([40, 41, 42], trailer[FrameType.TRAILER]); }, testTrailerOnly: function() { var arr = new Uint8Array([128, 0, 0, 0, 2, 40, 41]); var messages = parser.parse(arr.buffer); assertEquals(1, messages.length); var trailer = messages[0]; assertTrue(FrameType.TRAILER in trailer); assertElementsEquals([40, 41], trailer[FrameType.TRAILER]); }, testEmptyMessage: function() { var arr = new Uint8Array([0, 0, 0, 0, 0]); var messages = parser.parse(arr.buffer); assertEquals(1, messages.length); var message = messages[0]; assertTrue(FrameType.DATA in message); assertElementsEquals([], message[FrameType.DATA]); }, testEmptyMessageWithTrailer: function() { var arr = new Uint8Array([0, 0, 0, 0, 0, 128, 0, 0, 0, 1, 56]); var messages = parser.parse(arr.buffer); assertEquals(2, messages.length); var message = messages[0]; assertTrue(FrameType.DATA in message); assertElementsEquals([], message[FrameType.DATA]); var trailer = messages[1]; assertTrue(FrameType.TRAILER in trailer); assertElementsEquals([56], trailer[FrameType.TRAILER]); }, testErrorAfterFirstMessage: function() { var arr = new Uint8Array([0, 0, 0, 0, 2, 38, 39]); var messages = parser.parse(arr.buffer); assertEquals(1, messages.length); var message = messages[0]; assertTrue(FrameType.DATA in message); assertElementsEquals([38, 39], message[FrameType.DATA]); arr = new Uint8Array([1, 0]); assertThrows(function() { parser.parse(arr.buffer); }); }, testInvalidMessage: function() { var arr = new Uint8Array([0, 0, 0, 0, 2, 38, 39, 40, 0, 0, 0, 0, 2, 41, 42]); assertThrows(function() { parser.parse(arr.buffer); }); }, testEmptyArray: function() { var arr = new Uint8Array([]); var messages = parser.parse(arr.buffer); assertNull(messages); }, testMessageAfterEmptyArray: function() { var arr = new Uint8Array([]); var messages = parser.parse(arr.buffer); assertNull(messages); arr = new Uint8Array([]); messages = parser.parse(arr.buffer); assertNull(messages); arr = new Uint8Array([0]); messages = parser.parse(arr.buffer); assertNull(messages); arr = new Uint8Array([]); messages = parser.parse(arr.buffer); assertNull(messages); arr = new Uint8Array([0, 0, 0]); messages = parser.parse(arr.buffer); assertNull(messages); arr = new Uint8Array([]); messages = parser.parse(arr.buffer); assertNull(messages); arr = new Uint8Array([2, 38, 39]); messages = parser.parse(arr.buffer); assertEquals(1, messages.length); var message = messages[0]; assertTrue(FrameType.DATA in message); assertElementsEquals([38, 39], message[FrameType.DATA]); }, }); ================================================ FILE: javascript/net/grpc/web/interceptor.js ================================================ /** * @fileoverview grpc-web client interceptors. * * The type of interceptors is determined by the response type of the RPC call. * gRPC-Web has two generated clients for one service: * FooServiceClient and FooServicePromiseClient. The response type of * FooServiceClient is ClientReadableStream for BOTH unary calls and server * streaming calls, so StreamInterceptor is expected to be used for intercepting * FooServiceClient calls. The response type of PromiseClient is Promise, so use * UnaryInterceptor for PromiseClients. */ goog.module('grpc.web.Interceptor'); goog.module.declareLegacyNamespace(); const ClientReadableStream = goog.require('grpc.web.ClientReadableStream'); const Request = goog.require('grpc.web.Request'); const UnaryResponse = goog.require('grpc.web.UnaryResponse'); /** * Interceptor for RPC calls with response type `UnaryResponse`. * An example implementation of UnaryInterceptor *
 * TestUnaryInterceptor.prototype.intercept = function(request, invoker) {
 *   const newRequest = ...
 *   return invoker(newRequest).then((response) => {
 *     // Do something with response.getMetadata
       // Do something with response.getResponseMessage
 *     return response;
 *   });
 * };
 * 
* @interface */ const UnaryInterceptor = function() {}; /** * @export * @abstract * @template REQUEST, RESPONSE * @param {!Request} request * @param {function(!Request):!Promise>} * invoker * @return {!Promise>} */ UnaryInterceptor.prototype.intercept = function(request, invoker) {}; /** * Interceptor for RPC calls with response type `ClientReadableStream`. * * Two steps to create a stream interceptor: * <1>Create a new subclass of ClientReadableStream that wraps around the * original stream and overrides its methods. <2>Create a new subclass of * StreamInterceptor. While implementing the * StreamInterceptor.prototype.intercept method, return the wrapped * ClientReadableStream. * @interface */ const StreamInterceptor = function() {}; /** * @export * @abstract * @template REQUEST, RESPONSE * @param {!Request} request * @param {function(!Request):!ClientReadableStream} * invoker * @return {!ClientReadableStream} */ StreamInterceptor.prototype.intercept = function(request, invoker) {}; exports = { UnaryInterceptor, StreamInterceptor }; ================================================ FILE: javascript/net/grpc/web/metadata.js ================================================ /** * @fileoverview grpc-web request/response metadata. * * Request and response headers will be included in the Metadata. */ goog.module('grpc.web.Metadata'); goog.module.declareLegacyNamespace(); /** * @typedef {!Object} */ let Metadata; exports = Metadata; ================================================ FILE: javascript/net/grpc/web/methoddescriptor.js ================================================ /** * @fileoverview Description of this file. * * A templated class that is used to address gRPC Web requests. */ goog.module('grpc.web.MethodDescriptor'); goog.module.declareLegacyNamespace(); const CallOptions = goog.require('grpc.web.CallOptions'); const Metadata = goog.requireType('grpc.web.Metadata'); const MethodDescriptorInterface = goog.requireType('grpc.web.MethodDescriptorInterface'); const MethodType = goog.requireType('grpc.web.MethodType'); const Request = goog.requireType('grpc.web.Request'); const RequestInternal = goog.require('grpc.web.RequestInternal'); const UnaryResponse = goog.requireType('grpc.web.UnaryResponse'); const UnaryResponseInternal = goog.require('grpc.web.UnaryResponseInternal'); const {Status} = goog.requireType('grpc.web.Status'); /** * @final * @implements {MethodDescriptorInterface} * @template REQUEST, RESPONSE * @unrestricted */ const MethodDescriptor = class { /** * @param {string} name * @param {?MethodType} methodType * @param {function(new: REQUEST, ...)} requestType * @param {function(new: RESPONSE, ...)} responseType * @param {function(REQUEST): ?} requestSerializeFn * @param {function(?): RESPONSE} responseDeserializeFn */ constructor( name, methodType, requestType, responseType, requestSerializeFn, responseDeserializeFn) { /** @const */ this.name = name; /** @const */ this.methodType = methodType; /** @const */ this.requestType = requestType; /** @const */ this.responseType = responseType; /** @const */ this.requestSerializeFn = requestSerializeFn; /** @const */ this.responseDeserializeFn = responseDeserializeFn; } /** * @override * @param {REQUEST} requestMessage * @param {!Metadata=} metadata * @param {!CallOptions=} callOptions * @return {!Request} */ createRequest( requestMessage, metadata = {}, callOptions = new CallOptions()) { return new RequestInternal(requestMessage, this, metadata, callOptions); } /** * @override * @param {RESPONSE} responseMessage * @param {!Metadata=} metadata * @param {?Status=} status * @return {!UnaryResponse} */ createUnaryResponse(responseMessage, metadata = {}, status = null) { return new UnaryResponseInternal(responseMessage, this, metadata, status); } /** * @override * @export */ getName() { return this.name; } /** * @override */ getMethodType() { return this.methodType; } /** * @override * @return {function(new: RESPONSE, ...)} */ getResponseMessageCtor() { return this.responseType; } /** * @override * @return {function(new: REQUEST, ...)} */ getRequestMessageCtor() { return this.requestType; } /** @override */ getResponseDeserializeFn() { return this.responseDeserializeFn; } /** @override */ getRequestSerializeFn() { return this.requestSerializeFn; } }; exports = MethodDescriptor; ================================================ FILE: javascript/net/grpc/web/methoddescriptorinterface.js ================================================ /** * @fileoverview Description of this file. * * A templated class that is used to address gRPC Web requests. */ goog.module('grpc.web.MethodDescriptorInterface'); goog.module.declareLegacyNamespace(); const CallOptions = goog.requireType('grpc.web.CallOptions'); const Metadata = goog.requireType('grpc.web.Metadata'); const MethodType = goog.requireType('grpc.web.MethodType'); const Request = goog.requireType('grpc.web.Request'); const UnaryResponse = goog.requireType('grpc.web.UnaryResponse'); const {Status} = goog.requireType('grpc.web.Status'); /** * @interface * @template REQUEST, RESPONSE */ const MethodDescriptorInterface = function() {}; /** * @param {REQUEST} requestMessage * @param {!Metadata=} metadata * @param {!CallOptions=} callOptions * @return {!Request} */ MethodDescriptorInterface.prototype.createRequest = function( requestMessage, metadata, callOptions) {}; /** * @param {RESPONSE} responseMessage * @param {!Metadata=} metadata * @param {?Status=} status * @return {!UnaryResponse} */ MethodDescriptorInterface.prototype.createUnaryResponse = function( responseMessage, metadata, status) {}; /** @return {string} */ MethodDescriptorInterface.prototype.getName = function() {}; /** @return {?MethodType} */ MethodDescriptorInterface.prototype.getMethodType = function() {}; /** @return {function(new: RESPONSE, ?Array=)} */ MethodDescriptorInterface.prototype.getResponseMessageCtor = function() {}; /** @return {function(new: REQUEST, ?Array=)} */ MethodDescriptorInterface.prototype.getRequestMessageCtor = function() {}; /** @return {function(?): RESPONSE} */ MethodDescriptorInterface.prototype.getResponseDeserializeFn = function() {}; /** @return {function(REQUEST): ?} */ MethodDescriptorInterface.prototype.getRequestSerializeFn = function() {}; exports = MethodDescriptorInterface; ================================================ FILE: javascript/net/grpc/web/methodtype.js ================================================ /** * @fileoverview gRPC-Web method types. */ goog.module('grpc.web.MethodType'); goog.module.declareLegacyNamespace(); /** * Available method types: * MethodType.UNARY: unary request and unary response. * MethodType.SERVER_STREAMING: unary request and streaming responses. * MethodType.BIDI_STREAMING: streaming requests and streaming responses. * * @enum {string} */ const MethodType = { 'UNARY': 'unary', 'SERVER_STREAMING': 'server_streaming', // Bidi streaming is experimental. Do not use. 'BIDI_STREAMING': 'bidi_streaming', }; exports = MethodType; ================================================ FILE: javascript/net/grpc/web/request.js ================================================ /** * @fileoverview A templated class that is used to address an individual * gRPC-Web request instance. */ goog.module('grpc.web.Request'); goog.module.declareLegacyNamespace(); const CallOptions = goog.require('grpc.web.CallOptions'); const Metadata = goog.require('grpc.web.Metadata'); const MethodDescriptorInterface = goog.requireType('grpc.web.MethodDescriptorInterface'); /** * @interface * @template REQUEST, RESPONSE */ class Request { /** * @export * @return {REQUEST} */ getRequestMessage() {} /** * @export * @return {!MethodDescriptorInterface} */ getMethodDescriptor() {} /** * @export * @return {!Metadata} */ getMetadata() {} /** * Client CallOptions. Note that CallOptions has not been implemented in * grpc.web.AbstractClientbase yet, but will be used in * grpc.web.GenericClient. * @export * @return {!CallOptions|undefined} */ getCallOptions() {} /** * @param {string} key * @param {string} value * @return {!Request} */ withMetadata(key, value) {} /** * @param {string} name * @param {VALUE} value * @template VALUE * @return {!Request} */ withGrpcCallOption(name, value) {} } exports = Request; ================================================ FILE: javascript/net/grpc/web/requestinternal.js ================================================ /** * @fileoverview Internal implementation of grpc.web.Request. */ goog.module('grpc.web.RequestInternal'); goog.module.declareLegacyNamespace(); const CallOptions = goog.require('grpc.web.CallOptions'); const Metadata = goog.require('grpc.web.Metadata'); const MethodDescriptor = goog.requireType('grpc.web.MethodDescriptor'); const Request = goog.require('grpc.web.Request'); /** * @template REQUEST, RESPONSE * @implements {Request} * @final * @package */ class RequestInternal { /** * @param {REQUEST} requestMessage * @param {!MethodDescriptor} methodDescriptor * @param {!Metadata} metadata * @param {!CallOptions} callOptions */ constructor(requestMessage, methodDescriptor, metadata, callOptions) { /** * @const {REQUEST} * @private */ this.requestMessage_ = requestMessage; /** * @const {!MethodDescriptor} * @private */ this.methodDescriptor_ = methodDescriptor; /** @const @private */ this.metadata_ = metadata; /** @const @private */ this.callOptions_ = callOptions; } /** * @override * @return {REQUEST} */ getRequestMessage() { return this.requestMessage_; } /** * @override * @return {!MethodDescriptor} */ getMethodDescriptor() { return this.methodDescriptor_; } /** * @override * @return {!Metadata} */ getMetadata() { return this.metadata_; } /** * @override * @return {!CallOptions|undefined} */ getCallOptions() { return this.callOptions_; } /** * @override */ withMetadata(key, value) { this.metadata_[key] = value; return this; } /** * @override */ withGrpcCallOption(name, value) { this.callOptions_.setOption(name, value); return this; } } exports = RequestInternal; ================================================ FILE: javascript/net/grpc/web/rpcerror.js ================================================ /** * * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * @fileoverview gRPC-Web Error objects * * gRPC-Web Error objects * * @suppress {lintChecks} gRPC-Web is still using default goog.module exports * right now, and the output of grpc_generator.cc uses goog.provide. */ goog.module('grpc.web.RpcError'); const Metadata = goog.require('grpc.web.Metadata'); const StatusCode = goog.require('grpc.web.StatusCode'); /** * gRPC-Web Error object, contains the {@link StatusCode}, a string message * and {@link Metadata} contained in the error response. */ class RpcError extends Error { /** * @param {!StatusCode} code * @param {string} message * @param {!Metadata=} metadata */ constructor(code, message, metadata = {}) { super(message); /** @type {!StatusCode} */ this.code = code; /** @type {!Metadata} */ this.metadata = metadata; } /** @override */ toString() { const status = StatusCode.statusCodeName(this.code) || String(this.code); let out = `RpcError(${status})`; if (this.message) { out += ': ' + this.message; } return out; } } /** @override */ RpcError.prototype.name = 'RpcError'; exports = RpcError; ================================================ FILE: javascript/net/grpc/web/status.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * @fileoverview gRPC Web Status codes and mapping. * * gRPC Web Status codes and mapping. * * @author stanleycheung@google.com (Stanley Cheung) */ goog.module('grpc.web.Status'); goog.module.declareLegacyNamespace(); /** @record */ function Status() {} /** @export {number} */ Status.prototype.code; /** @export {string} */ Status.prototype.details; /** @export {(!Object|undefined)} */ Status.prototype.metadata; exports.Status = Status; ================================================ FILE: javascript/net/grpc/web/statuscode.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * @fileoverview gRPC Web Status codes and mapping. * * gRPC Web Status codes and mapping. * * @author stanleycheung@google.com (Stanley Cheung) */ goog.module('grpc.web.StatusCode'); /** * gRPC Status Codes * See: * https://github.com/grpc/grpc/blob/master/include/grpcpp/impl/codegen/status_code_enum.h * @enum {number} */ const StatusCode = { // LINT.IfChange(status_codes) // Not an error; returned on success. 'OK': 0, // The operation was cancelled (typically by the caller). 'CANCELLED': 1, // Unknown error. An example of where this error may be returned is if a // Status value received from another address space belongs to an error-space // that is not known in this address space. Also errors raised by APIs that // do not return enough error information may be converted to this error. 'UNKNOWN': 2, // Client specified an invalid argument. Note that this differs from // FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments that are // problematic regardless of the state of the system (e.g., a malformed file // name). 'INVALID_ARGUMENT': 3, // Deadline expired before operation could complete. For operations that // change the state of the system, this error may be returned even if the // operation has completed successfully. For example, a successful response // from a server could have been delayed long enough for the deadline to // expire. 'DEADLINE_EXCEEDED': 4, // Some requested entity (e.g., file or directory) was not found. 'NOT_FOUND': 5, // Some entity that we attempted to create (e.g., file or directory) already // exists. 'ALREADY_EXISTS': 6, // The caller does not have permission to execute the specified operation. // PERMISSION_DENIED must not be used for rejections caused by exhausting // some resource (use RESOURCE_EXHAUSTED instead for those errors). // PERMISSION_DENIED must not be used if the caller can not be identified // (use UNAUTHENTICATED instead for those errors). 'PERMISSION_DENIED': 7, // The request does not have valid authentication credentials for the // operation. 'UNAUTHENTICATED': 16, // Some resource has been exhausted, perhaps a per-user quota, or perhaps the // entire file system is out of space. 'RESOURCE_EXHAUSTED': 8, // Operation was rejected because the system is not in a state required for // the operation's execution. For example, directory to be deleted may be // non-empty, an rmdir operation is applied to a non-directory, etc. // // A litmus test that may help a service implementor in deciding // between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE: // (a) Use UNAVAILABLE if the client can retry just the failing call. // (b) Use ABORTED if the client should retry at a higher-level // (e.g., restarting a read-modify-write sequence). // (c) Use FAILED_PRECONDITION if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, FAILED_PRECONDITION // should be returned since the client should not retry unless // they have first fixed up the directory by deleting files from it. // (d) Use FAILED_PRECONDITION if the client performs conditional // REST Get/Update/Delete on a resource and the resource on the // server does not match the condition. E.g., conflicting // read-modify-write on the same resource. 'FAILED_PRECONDITION': 9, // The operation was aborted, typically due to a concurrency issue like // sequencer check failures, transaction aborts, etc. // // See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, // and UNAVAILABLE. 'ABORTED': 10, // Operation was attempted past the valid range. E.g., seeking or reading // past end of file. // // Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed // if the system state changes. For example, a 32-bit file system will // generate INVALID_ARGUMENT if asked to read at an offset that is not in the // range [0,2^32-1], but it will generate OUT_OF_RANGE if asked to read from // an offset past the current file size. // // There is a fair bit of overlap between FAILED_PRECONDITION and // OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error) // when it applies so that callers who are iterating through a space can // easily look for an OUT_OF_RANGE error to detect when they are done. 'OUT_OF_RANGE': 11, // Operation is not implemented or not supported/enabled in this service. 'UNIMPLEMENTED': 12, // Internal errors. Means some invariants expected by underlying System has // been broken. If you see one of these errors, Something is very broken. 'INTERNAL': 13, // The service is currently unavailable. This is a most likely a transient // condition and may be corrected by retrying with a backoff. // // See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, // and UNAVAILABLE. 'UNAVAILABLE': 14, // Unrecoverable data loss or corruption. 'DATA_LOSS': 15, // LINT.ThenChange(:status_code_name) }; /** * Convert HTTP Status code to gRPC Status code * @param {number} httpStatus HTTP Status Code * @return {!StatusCode} gRPC Status Code */ StatusCode.fromHttpStatus = function(httpStatus) { switch (httpStatus) { case 200: return StatusCode.OK; case 400: return StatusCode.INVALID_ARGUMENT; case 401: return StatusCode.UNAUTHENTICATED; case 403: return StatusCode.PERMISSION_DENIED; case 404: return StatusCode.NOT_FOUND; case 409: return StatusCode.ABORTED; case 412: return StatusCode.FAILED_PRECONDITION; case 429: return StatusCode.RESOURCE_EXHAUSTED; case 499: return StatusCode.CANCELLED; case 500: return StatusCode.UNKNOWN; case 501: return StatusCode.UNIMPLEMENTED; case 503: return StatusCode.UNAVAILABLE; case 504: return StatusCode.DEADLINE_EXCEEDED; /* everything else is unknown */ default: return StatusCode.UNKNOWN; } }; /** * Convert a {@link StatusCode} to an HTTP Status code * @param {!StatusCode} statusCode GRPC Status Code * @return {number} HTTP Status code */ StatusCode.getHttpStatus = function(statusCode) { switch (statusCode) { case StatusCode.OK: return 200; case StatusCode.INVALID_ARGUMENT: return 400; case StatusCode.UNAUTHENTICATED: return 401; case StatusCode.PERMISSION_DENIED: return 403; case StatusCode.NOT_FOUND: return 404; case StatusCode.ABORTED: return 409; case StatusCode.FAILED_PRECONDITION: return 412; case StatusCode.RESOURCE_EXHAUSTED: return 429; case StatusCode.CANCELLED: return 499; case StatusCode.UNKNOWN: return 500; case StatusCode.UNIMPLEMENTED: return 501; case StatusCode.UNAVAILABLE: return 503; case StatusCode.DEADLINE_EXCEEDED: return 504; /* everything else is unknown */ default: return 0; } }; /** * Returns the human readable name for a {@link StatusCode}. Useful for logging. * @param {!StatusCode} statusCode GRPC Status Code * @return {string} the human readable name for the status code */ StatusCode.statusCodeName = function(statusCode) { switch (statusCode) { // LINT.IfChange(status_code_name) case StatusCode.OK: return 'OK'; case StatusCode.CANCELLED: return 'CANCELLED'; case StatusCode.UNKNOWN: return 'UNKNOWN'; case StatusCode.INVALID_ARGUMENT: return 'INVALID_ARGUMENT'; case StatusCode.DEADLINE_EXCEEDED: return 'DEADLINE_EXCEEDED'; case StatusCode.NOT_FOUND: return 'NOT_FOUND'; case StatusCode.ALREADY_EXISTS: return 'ALREADY_EXISTS'; case StatusCode.PERMISSION_DENIED: return 'PERMISSION_DENIED'; case StatusCode.UNAUTHENTICATED: return 'UNAUTHENTICATED'; case StatusCode.RESOURCE_EXHAUSTED: return 'RESOURCE_EXHAUSTED'; case StatusCode.FAILED_PRECONDITION: return 'FAILED_PRECONDITION'; case StatusCode.ABORTED: return 'ABORTED'; case StatusCode.OUT_OF_RANGE: return 'OUT_OF_RANGE'; case StatusCode.UNIMPLEMENTED: return 'UNIMPLEMENTED'; case StatusCode.INTERNAL: return 'INTERNAL'; case StatusCode.UNAVAILABLE: return 'UNAVAILABLE'; case StatusCode.DATA_LOSS: return 'DATA_LOSS'; default: return ''; // LINT.ThenChange(:status_codes) } }; exports = StatusCode; ================================================ FILE: javascript/net/grpc/web/statuscode_test.js ================================================ goog.module('grpc.web.StatusCodeTest'); goog.setTestOnly('grpc.web.StatusCodeTest'); const StatusCode = goog.require('grpc.web.StatusCode'); const testSuite = goog.require('goog.testing.testSuite'); /** @type {!Map} */ const statusMap = new Map([ [200, StatusCode.OK], [400, StatusCode.INVALID_ARGUMENT], [401, StatusCode.UNAUTHENTICATED], [403, StatusCode.PERMISSION_DENIED], [404, StatusCode.NOT_FOUND], [409, StatusCode.ABORTED], [412, StatusCode.FAILED_PRECONDITION], [429, StatusCode.RESOURCE_EXHAUSTED], [500, StatusCode.UNKNOWN], [501, StatusCode.UNIMPLEMENTED], [503, StatusCode.UNAVAILABLE], [504, StatusCode.DEADLINE_EXCEEDED], ]); testSuite({ testFromHttpStatus() { statusMap.forEach((statusCode, httpStatus) => { assertEquals(StatusCode.fromHttpStatus(httpStatus), statusCode); }); }, testGetHttpStatus() { statusMap.forEach((statusCode, httpStatus) => { assertEquals(StatusCode.getHttpStatus(statusCode), httpStatus); }); }, testUnknown() { assertEquals(StatusCode.getHttpStatus(StatusCode.UNKNOWN), 500); assertEquals(StatusCode.fromHttpStatus(511), StatusCode.UNKNOWN); } }); ================================================ FILE: javascript/net/grpc/web/unaryresponse.js ================================================ /** * @fileoverview gRPC web client UnaryResponse returned by grpc unary calls. */ goog.module('grpc.web.UnaryResponse'); goog.module.declareLegacyNamespace(); const Metadata = goog.requireType('grpc.web.Metadata'); const MethodDescriptorInterface = goog.requireType('grpc.web.MethodDescriptorInterface'); const {Status} = goog.requireType('grpc.web.Status'); /** * @interface * @template REQUEST, RESPONSE */ class UnaryResponse { /** * @export * @return {RESPONSE} */ getResponseMessage() {} /** * @export * @return {!Metadata} */ getMetadata() {} /** * @export * @return {!MethodDescriptorInterface} */ getMethodDescriptor() {} /** * gRPC status. Trailer metadata returned from a gRPC server is in * status.metadata. * @export * @return {?Status} */ getStatus() {} } exports = UnaryResponse; ================================================ FILE: javascript/net/grpc/web/unaryresponseinternal.js ================================================ /** * @fileoverview gRPC-Web UnaryResponse internal implementation. */ goog.module('grpc.web.UnaryResponseInternal'); goog.module.declareLegacyNamespace(); const Metadata = goog.requireType('grpc.web.Metadata'); const MethodDescriptor = goog.requireType('grpc.web.MethodDescriptor'); const UnaryResponse = goog.requireType('grpc.web.UnaryResponse'); const {Status} = goog.requireType('grpc.web.Status'); /** * @template REQUEST, RESPONSE * @implements {UnaryResponse} * @final * @package */ class UnaryResponseInternal { /** * @param {RESPONSE} responseMessage * @param {!MethodDescriptor} methodDescriptor * @param {!Metadata=} metadata * @param {?Status=} status */ constructor(responseMessage, methodDescriptor, metadata = {}, status = null) { /** * @const {RESPONSE} * @private */ this.responseMessage_ = responseMessage; /** * @const {!Metadata} * @private */ this.metadata_ = metadata; /** * @const {!MethodDescriptor} * @private */ this.methodDescriptor_ = methodDescriptor; /** * @const {?Status} * @private */ this.status_ = status; } /** @override */ getResponseMessage() { return this.responseMessage_; } /** @override */ getMetadata() { return this.metadata_; } /** @override */ getMethodDescriptor() { return this.methodDescriptor_; } /** @override */ getStatus() { return this.status_; } } exports = UnaryResponseInternal; ================================================ FILE: kokoro/interop.cfg ================================================ build_file: "grpc-web/scripts/run_interop_tests.sh" ================================================ FILE: kokoro/master.cfg ================================================ build_file: "grpc-web/scripts/kokoro.sh" ================================================ FILE: kokoro/presubmit.cfg ================================================ build_file: "grpc-web/scripts/run_basic_tests.sh" ================================================ FILE: net/grpc/gateway/docker/binary_client/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM grpcweb/prereqs WORKDIR /github/grpc-web/net/grpc/gateway/examples/echo RUN protoc -I=. echo.proto \ --js_out=import_style=commonjs:./commonjs-example \ --grpc-web_out=import_style=commonjs,mode=grpcweb:./commonjs-example WORKDIR /github/grpc-web/net/grpc/gateway/examples/echo/commonjs-example RUN npm install && \ npm link grpc-web && \ npx webpack && \ cp echotest.html /var/www/html && \ cp dist/main.js /var/www/html/dist WORKDIR /var/www/html EXPOSE 8081 CMD ["python", "-m", "SimpleHTTPServer", "8081"] ================================================ FILE: net/grpc/gateway/docker/closure_client/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM grpcweb/prereqs WORKDIR /github/grpc-web/net/grpc/gateway/examples/echo RUN npm install RUN make client && make install WORKDIR /var/www/html EXPOSE 8081 CMD ["python", "-m", "SimpleHTTPServer", "8081"] ================================================ FILE: net/grpc/gateway/docker/commonjs_client/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM grpcweb/prereqs WORKDIR /github/grpc-web/net/grpc/gateway/examples/echo RUN protoc -I=. echo.proto \ --js_out=import_style=commonjs:./commonjs-example \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:./commonjs-example WORKDIR /github/grpc-web/net/grpc/gateway/examples/echo/commonjs-example RUN npm install && \ npm link grpc-web && \ npx webpack && \ cp echotest.html /var/www/html && \ cp dist/main.js /var/www/html/dist WORKDIR /var/www/html EXPOSE 8081 CMD ["python", "-m", "SimpleHTTPServer", "8081"] ================================================ FILE: net/grpc/gateway/docker/echo_server/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM grpcweb/prereqs WORKDIR /github/grpc-web RUN bazel build net/grpc/gateway/examples/echo:server WORKDIR /github/grpc-web/bazel-bin/net/grpc/gateway/examples/echo EXPOSE 9090 CMD ["./server"] ================================================ FILE: net/grpc/gateway/docker/envoy/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM envoyproxy/envoy:v1.22.0 COPY net/grpc/gateway/examples/echo/envoy.yaml /etc/envoy/envoy.yaml ENTRYPOINT [ "/usr/local/bin/envoy" ] CMD [ "-c /etc/envoy/envoy.yaml", "-l trace", "--log-path /tmp/envoy_info.log" ] ================================================ FILE: net/grpc/gateway/docker/grpcwebproxy/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM golang:1.16-alpine3.13 RUN apk add --no-cache curl git ca-certificates && \ rm -rf /var/lib/apt/lists/* ARG VERSION=0.14.0 WORKDIR /tmp RUN curl -sS https://raw.githubusercontent.com/golang/dep/master/install.sh | sh RUN wget https://github.com/improbable-eng/grpc-web/archive/v$VERSION.tar.gz WORKDIR /go/src/github.com/improbable-eng/ RUN tar -zxf /tmp/v$VERSION.tar.gz -C . RUN mv grpc-web-$VERSION grpc-web WORKDIR /go/src/github.com/improbable-eng/grpc-web RUN dep ensure && \ go env -w GO111MODULE=auto && \ go install ./go/grpcwebproxy ADD ./etc/localhost.crt /etc ADD ./etc/localhost.key /etc ENTRYPOINT [ "/bin/sh", "-c", "exec /go/bin/grpcwebproxy \ --backend_addr=node-server:9090 \ --run_tls_server=false \ --allow_all_origins " ] ================================================ FILE: net/grpc/gateway/docker/interop_client/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM grpcweb/prereqs WORKDIR /github/grpc-web/test/interop COPY ./test/interop . RUN protoc -I=../.. src/proto/grpc/testing/test.proto \ src/proto/grpc/testing/empty.proto src/proto/grpc/testing/messages.proto \ --js_out=import_style=commonjs:. \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:. RUN npm install && \ npm link grpc-web && \ npx webpack && \ cp index.html /var/www/html && \ cp dist/main.js /var/www/html/dist WORKDIR /var/www/html EXPOSE 8081 CMD ["python", "-m", "SimpleHTTPServer", "8081"] ================================================ FILE: net/grpc/gateway/docker/node_interop_server/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM node:20.0.0-bullseye WORKDIR /github/grpc-node RUN git clone https://github.com/grpc/grpc-node . && \ git submodule --quiet update --init --recursive RUN cd packages/grpc-native-core && \ npm install --build-from-source --unsafe-perm && \ npm link RUN cd packages/proto-loader && \ npm install @types/mocha@7.0.2&& \ npm install --unsafe-perm WORKDIR /github/grpc-node/test RUN npm install node-pre-gyp && \ npm install && \ npm link grpc EXPOSE 7074 CMD ["node", "--require", "./fixtures/native_native", "./interop/interop_server.js", "--port=7074"] ================================================ FILE: net/grpc/gateway/docker/node_server/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM grpcweb/prereqs WORKDIR /github/grpc-web/net/grpc/gateway/examples/echo/node-server RUN npm install EXPOSE 9090 CMD ["node", "server.js"] ================================================ FILE: net/grpc/gateway/docker/prereqs/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Using multi-stage buid (See: https://docs.docker.com/develop/develop-images/multistage-build/) ###################################### # Stage 1: Fetch binaries ###################################### # node:... Docker image is based on buildpack-deps:bullseye (11) FROM buildpack-deps:bullseye AS prepare ARG BUILDIFIER_VERSION=1.0.0 ARG PROTOBUF_VERSION=3.19.4 RUN apt-get -qq update && apt-get -qq install -y curl unzip WORKDIR /tmp RUN curl -sSL https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOBUF_VERSION/\ protoc-$PROTOBUF_VERSION-linux-x86_64.zip -o protoc.zip && \ unzip -qq protoc.zip && \ cp ./bin/protoc /usr/local/bin/protoc RUN wget -nv -O buildifier \ https://github.com/bazelbuild/buildtools/releases/download/$BUILDIFIER_VERSION/buildifier && \ chmod +x ./buildifier && \ cp ./buildifier /usr/local/bin/buildifier # Download third_party modules to be used for the next stage WORKDIR /github/grpc-web RUN git clone https://github.com/grpc/grpc-web . COPY ./scripts/init_submodules.sh ./scripts/ RUN ./scripts/init_submodules.sh ###################################### # Stage 2: Copy source files and build ###################################### FROM node:20.0.0-bullseye AS copy-and-build ARG MAKEFLAGS=-j8 ARG BAZEL_VERSION=7.3.0 RUN apt-get -qq update && apt-get -qq install -y python RUN mkdir -p /var/www/html/dist RUN echo "\nloglevel=error\n" >> $HOME/.npmrc COPY --from=prepare /usr/local/bin/protoc /usr/local/bin/ COPY --from=prepare /usr/local/bin/buildifier /usr/local/bin/ COPY --from=prepare /github/grpc-web/third_party /github/grpc-web/third_party RUN wget -nv -O bazel-installer.sh \ https://github.com/bazelbuild/bazel/releases/download/$BAZEL_VERSION/\ bazel-$BAZEL_VERSION-installer-linux-x86_64.sh && \ chmod +x ./bazel-installer.sh && \ ./bazel-installer.sh && \ rm ./bazel-installer.sh WORKDIR /github/grpc-web # Copy only files necessary to build the protoc-gen-grpc-web first as an optimization because they # are rarely updated compared with the javascript files. COPY ./MODULE.bazel ./MODULE.bazel COPY ./MODULE.bazel.lock ./MODULE.bazel.lock COPY ./.bazelrc ./.bazelrc COPY ./javascript/net/grpc/web/generator javascript/net/grpc/web/generator # Pre-build the protobuf compiler lib so the following plugin build can reuse Bazel cache. RUN bazel build "@com_google_protobuf//:protoc_lib" RUN bazel build javascript/net/grpc/web/generator:protoc-gen-grpc-web && \ cp $(bazel info bazel-genfiles)/javascript/net/grpc/web/generator/protoc-gen-grpc-web \ /usr/local/bin/protoc-gen-grpc-web COPY ./javascript ./javascript COPY ./packages ./packages RUN cd ./packages/grpc-web && \ npm install && \ npm run build && \ npm link COPY ./Makefile ./Makefile COPY ./net ./net COPY ./scripts ./scripts COPY ./src ./src COPY ./test ./test RUN /usr/local/bin/buildifier \ --mode=check --lint=warn --warnings=all -r ./MODULE.bazel javascript net ================================================ FILE: net/grpc/gateway/docker/ts_client/Dockerfile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM grpcweb/prereqs WORKDIR /github/grpc-web # Bring in current workspace COPY ./net/grpc/gateway/examples/echo ./net/grpc/gateway/examples/echo WORKDIR /github/grpc-web/net/grpc/gateway/examples/echo RUN protoc -I=. echo.proto \ --js_out=import_style=commonjs:./ts-example \ --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:./ts-example WORKDIR /github/grpc-web/net/grpc/gateway/examples/echo/ts-example RUN npm install && \ npm link grpc-web && \ npx tsc && \ # Since typescript@4.5.2, tsc has apparently stopped moving dependent js files into dist/ dir, so # we'll move them manually. mv *_pb.js dist/ && \ npx webpack && \ cp echotest.html /var/www/html && \ cp dist/main.js /var/www/html/dist WORKDIR /var/www/html EXPOSE 8081 CMD ["python", "-m", "SimpleHTTPServer", "8081"] ================================================ FILE: net/grpc/gateway/examples/echo/.gitignore ================================================ /package-lock.json /node_modules ================================================ FILE: net/grpc/gateway/examples/echo/BUILD.bazel ================================================ load("@com_github_grpc_grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library") load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_proto_library") load("@rules_proto//proto:defs.bzl", "proto_library") proto_library( name = "echo_proto", srcs = [ "echo.proto", ], ) # Server cc_proto_library( name = "echo_cc_proto", deps = [ ":echo_proto", ], ) cc_grpc_library( name = "echo_cc_grpc", srcs = [ ":echo_proto", ], grpc_only = True, deps = [ ":echo_cc_proto", ], ) cc_binary( name = "server", srcs = [ "echo_server.cc", "echo_service_impl.cc", "echo_service_impl.h", ], deps = [ ":echo_cc_grpc", ":echo_cc_proto", "@com_github_grpc_grpc//:grpc++", ], ) ================================================ FILE: net/grpc/gateway/examples/echo/Makefile ================================================ # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ROOT_DIR = ../../../../.. NPM_DIR = ./node_modules PROTOC = protoc PROTOS_PATH = ../.. HTML_DIR = /var/www/html JS_IMPORT_STYLE = import_style=closure,binary JS_PATH = javascript/net/grpc/web OUT_DIR = ./out PROTOBUF_PATH = third_party/protobuf GRPC_WEB_PLUGIN_PATH = /usr/local/bin/protoc-gen-grpc-web all: client client: proto-js compiled-js compiled-js: ./node_modules/.bin/google-closure-compiler \ --js=*.js \ --js=$(OUT_DIR)/*.js \ --js=$(ROOT_DIR)/javascript \ --js=$(ROOT_DIR)/$(PROTOBUF_PATH)/js \ --js='!$(ROOT_DIR)/$(PROTOBUF_PATH)/js/**/*_test.js' \ --js=$(NPM_DIR)/google-closure-library \ --entry_point=goog:proto.grpc.gateway.testing.EchoServiceClient \ --dependency_mode=PRUNE \ --js_output_file compiled.js proto-js: mkdir -p $(OUT_DIR) $(PROTOC) -I=$(ROOT_DIR)/$(PROTOBUF_PATH)/src/google/protobuf \ --js_out=$(JS_IMPORT_STYLE):$(OUT_DIR) \ $(ROOT_DIR)/$(PROTOBUF_PATH)/src/google/protobuf/any.proto $(PROTOC) -I=. --js_out=$(JS_IMPORT_STYLE):$(OUT_DIR) ./echo.proto $(PROTOC) -I=. --plugin=protoc-gen-grpc-web=$(GRPC_WEB_PLUGIN_PATH) \ --grpc-web_out=import_style=closure,mode=grpcwebtext:. ./echo.proto install: mkdir -p $(HTML_DIR) cp ./echotest.html $(HTML_DIR) cp ./echoapp.js $(HTML_DIR) cp ./compiled.js $(HTML_DIR)/echo_js_bin_dev.js clean: rm -f compiled.js rm -f *_pb.js rm -rf $(OUT_DIR) ================================================ FILE: net/grpc/gateway/examples/echo/README.md ================================================ ## Build and Run an Echo example This page will show you how to quickly build and run an end-to-end Echo example. The example has 3 key components: - Front-end JS client - Envoy proxy - gRPC backend server (written in Node) From the repo root directory: ## Build pre-requisites This step downloads the necessary pre-requisites, and serves as the base docker image for the subsequent docker images. ```sh $ docker build -t grpcweb/prereqs \ -f net/grpc/gateway/docker/prereqs/Dockerfile . ``` ## Run the gRPC Backend server This compiles the gRPC backend server, written in Node, and listens on port 9090. ```sh $ docker build -t grpcweb/node-server \ -f net/grpc/gateway/docker/node_server/Dockerfile . $ docker run -d -p 9090:9090 --name node-server grpcweb/node-server ``` ## Run the Envoy proxy This step runs the Envoy proxy, and listens on port 8080. Any gRPC-Web browser requests will be forwarded to port 9090. ```sh $ docker build -t grpcweb/envoy \ -f net/grpc/gateway/docker/envoy/Dockerfile . $ docker run -d -p 8080:8080 --link node-server:node-server grpcweb/envoy ``` ## Serve static JS/HTML contents This steps compiles the front-end gRPC-Web client into a static .JS file, and we use a simple server to serve up the JS/HTML static contents. ```sh $ docker build -t grpcweb/commonjs-client \ -f net/grpc/gateway/docker/commonjs_client/Dockerfile . $ docker run -d -p 8081:8081 grpcweb/commonjs-client ``` ## Run the example from your browser Finally, open a browser tab, and inspect ``` http://localhost:8081/echotest.html ``` ## What's next? For more details about how you can run your own gRPC service and access it from the browser, please see this [tutorial](tutorial.md) ================================================ FILE: net/grpc/gateway/examples/echo/commonjs-example/.gitignore ================================================ node_modules package-lock.json ================================================ FILE: net/grpc/gateway/examples/echo/commonjs-example/client.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const {EchoRequest, ServerStreamingEchoRequest} = require('./echo_pb.js'); const {EchoServiceClient} = require('./echo_grpc_web_pb.js'); const {EchoApp} = require('../echoapp.js'); const grpc = {}; grpc.web = require('grpc-web'); /** Sample interceptor implementation */ const StreamResponseInterceptor = function() {}; /** * @template REQUEST, RESPONSE * @param {!Request} request * @param {function(!Request):!ClientReadableStream} * invoker * @return {!ClientReadableStream} */ StreamResponseInterceptor.prototype.intercept = function(request, invoker) { const InterceptedStream = function(stream) { this.stream = stream; }; InterceptedStream.prototype.on = function(eventType, callback) { if (eventType == 'data') { const newCallback = (response) => { response.setMessage('[Intcpt Resp1]'+response.getMessage()); callback(response); }; this.stream.on(eventType, newCallback); } else { this.stream.on(eventType, callback); } return this; }; var reqMsg = request.getRequestMessage(); reqMsg.setMessage('[Intcpt Req1]'+reqMsg.getMessage()); return new InterceptedStream(invoker(request)); }; var opts = {'streamInterceptors' : [new StreamResponseInterceptor()]}; var echoService = new EchoServiceClient('http://'+window.location.hostname+':8080', null, null); // opts); var echoApp = new EchoApp( echoService, { EchoRequest: EchoRequest, ServerStreamingEchoRequest: ServerStreamingEchoRequest } ); echoApp.load(); ================================================ FILE: net/grpc/gateway/examples/echo/commonjs-example/echotest.html ================================================ Echo Example

Example: "Hello", "4 Hello", "err Hello"

================================================ FILE: net/grpc/gateway/examples/echo/commonjs-example/package.json ================================================ { "name": "grpc-web-commonjs-example", "version": "0.1.0", "description": "gRPC-Web CommonJS client example", "license": "Apache-2.0", "dependencies": { "google-protobuf": "~3.21.4", "grpc-web": "~2.0.2" }, "devDependencies": { "webpack": "5.101.3", "webpack-cli": "~5.1.1" } } ================================================ FILE: net/grpc/gateway/examples/echo/commonjs-example/webpack.config.js ================================================ module.exports = { mode: "production", entry: "./client.js", }; ================================================ FILE: net/grpc/gateway/examples/echo/echo.proto ================================================ // Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package grpc.gateway.testing; message Empty {} message EchoRequest { string message = 1; } message EchoResponse { string message = 1; int32 message_count = 2; } // Request type for server side streaming echo. message ServerStreamingEchoRequest { // Message string for server streaming request. string message = 1; // The total number of messages to be generated before the server // closes the stream; default is 10. int32 message_count = 2; // The interval (ms) between two server messages. The server implementation // may enforce some minimum interval (e.g. 100ms) to avoid message overflow. int32 message_interval = 3; } // Response type for server streaming response. message ServerStreamingEchoResponse { // Response message. string message = 1; } // Request type for client side streaming echo. message ClientStreamingEchoRequest { // A special value "" indicates that there's no further messages. string message = 1; } // Response type for client side streaming echo. message ClientStreamingEchoResponse { // Total number of client messages that have been received. int32 message_count = 1; } // A simple echo service. service EchoService { // One request followed by one response // The server returns the client message as-is. rpc Echo(EchoRequest) returns (EchoResponse); // Sends back abort status. rpc EchoAbort(EchoRequest) returns (EchoResponse) {} // One empty request, ZERO processing, followed by one empty response // (minimum effort to do message serialization). rpc NoOp(Empty) returns (Empty); // One request followed by a sequence of responses (streamed download). // The server will return the same client message repeatedly. rpc ServerStreamingEcho(ServerStreamingEchoRequest) returns (stream ServerStreamingEchoResponse); // One request followed by a sequence of responses (streamed download). // The server abort directly. rpc ServerStreamingEchoAbort(ServerStreamingEchoRequest) returns (stream ServerStreamingEchoResponse) {} // A sequence of requests followed by one response (streamed upload). // The server returns the total number of messages as the result. // Notice: Client side streaming and Bidi streaming are not supported at the moment. rpc ClientStreamingEcho(stream ClientStreamingEchoRequest) returns (ClientStreamingEchoResponse); // A sequence of requests with each message echoed by the server immediately. // The server returns the same client messages in order. // E.g. this is how the speech API works. // Notice: Client side streaming and Bidi streaming are not supported at the moment. rpc FullDuplexEcho(stream EchoRequest) returns (stream EchoResponse); // A sequence of requests followed by a sequence of responses. // The server buffers all the client messages and then returns the same // client messages one by one after the client half-closes the stream. // This is how an image recognition API may work. // Notice: Client side streaming and Bidi streaming are not supported at the moment. rpc HalfDuplexEcho(stream EchoRequest) returns (stream EchoResponse); } ================================================ FILE: net/grpc/gateway/examples/echo/echo_chat.js ================================================ // TODO: Implement simple chat client using ClosureJS (similar to echoapp.js). ================================================ FILE: net/grpc/gateway/examples/echo/echo_server.cc ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #include #include #include #include "net/grpc/gateway/examples/echo/echo.grpc.pb.h" #include "net/grpc/gateway/examples/echo/echo_service_impl.h" using grpc::Server; using grpc::ServerBuilder; void RunServer() { std::string server_address("0.0.0.0:9090"); EchoServiceImpl service; ServerBuilder builder; builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); builder.RegisterService(&service); std::unique_ptr server(builder.BuildAndStart()); server->Wait(); } int main(int argc, char** argv) { RunServer(); return 0; } ================================================ FILE: net/grpc/gateway/examples/echo/echo_service_impl.cc ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #include "net/grpc/gateway/examples/echo/echo_service_impl.h" #include #include #include #include "net/grpc/gateway/examples/echo/echo.grpc.pb.h" using grpc::ServerContext; using grpc::ServerWriter; using grpc::Status; using grpc::gateway::testing::EchoRequest; using grpc::gateway::testing::EchoResponse; using grpc::gateway::testing::EchoService; using grpc::gateway::testing::Empty; using grpc::gateway::testing::ServerStreamingEchoRequest; using grpc::gateway::testing::ServerStreamingEchoResponse; EchoServiceImpl::EchoServiceImpl() {} EchoServiceImpl::~EchoServiceImpl() {} void EchoServiceImpl::CopyClientMetadataToResponse(ServerContext* context) { for (auto& client_metadata : context->client_metadata()) { context->AddInitialMetadata(std::string(client_metadata.first.data(), client_metadata.first.length()), std::string(client_metadata.second.data(), client_metadata.second.length())); context->AddTrailingMetadata( std::string(client_metadata.first.data(), client_metadata.first.length()), std::string(client_metadata.second.data(), client_metadata.second.length())); } } Status EchoServiceImpl::Echo(ServerContext* context, const EchoRequest* request, EchoResponse* response) { CopyClientMetadataToResponse(context); response->set_message(request->message()); return Status::OK; } Status EchoServiceImpl::EchoAbort(ServerContext* context, const EchoRequest* request, EchoResponse* response) { CopyClientMetadataToResponse(context); response->set_message(request->message()); return Status(grpc::StatusCode::ABORTED, "Aborted from server side."); } Status EchoServiceImpl::NoOp(ServerContext* context, const Empty* request, Empty* response) { CopyClientMetadataToResponse(context); return Status::OK; } Status EchoServiceImpl::ServerStreamingEcho( ServerContext* context, const ServerStreamingEchoRequest* request, ServerWriter* writer) { CopyClientMetadataToResponse(context); for (int i = 0; i < request->message_count(); i++) { if (context->IsCancelled()) { return Status::CANCELLED; } ServerStreamingEchoResponse response; response.set_message(request->message()); usleep(request->message_interval() * 1000); writer->Write(response); } return Status::OK; } Status EchoServiceImpl::ServerStreamingEchoAbort( ServerContext* context, const ServerStreamingEchoRequest* request, ServerWriter* writer) { CopyClientMetadataToResponse(context); ServerStreamingEchoResponse response; response.set_message(request->message()); writer->Write(response); return Status(grpc::StatusCode::ABORTED, "Aborted from server side."); } ================================================ FILE: net/grpc/gateway/examples/echo/echo_service_impl.h ================================================ #ifndef NET_GRPC_GATEWAY_EXAMPLES_ECHO_ECHO_SERVICE_IMPL_H_ #define NET_GRPC_GATEWAY_EXAMPLES_ECHO_ECHO_SERVICE_IMPL_H_ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #include #include #include #include "net/grpc/gateway/examples/echo/echo.grpc.pb.h" class EchoServiceImpl final : public grpc::gateway::testing::EchoService::Service { public: EchoServiceImpl(); ~EchoServiceImpl() override; void CopyClientMetadataToResponse(grpc::ServerContext* context); grpc::Status Echo( grpc::ServerContext* context, const grpc::gateway::testing::EchoRequest* request, grpc::gateway::testing::EchoResponse* response) override; grpc::Status EchoAbort( grpc::ServerContext* context, const grpc::gateway::testing::EchoRequest* request, grpc::gateway::testing::EchoResponse* response) override; grpc::Status NoOp( grpc::ServerContext* context, const grpc::gateway::testing::Empty* request, grpc::gateway::testing::Empty* response) override; grpc::Status ServerStreamingEcho( grpc::ServerContext* context, const grpc::gateway::testing::ServerStreamingEchoRequest* request, grpc::ServerWriter< grpc::gateway::testing::ServerStreamingEchoResponse>* writer) override; grpc::Status ServerStreamingEchoAbort( grpc::ServerContext* context, const grpc::gateway::testing::ServerStreamingEchoRequest* request, grpc::ServerWriter< grpc::gateway::testing::ServerStreamingEchoResponse>* writer) override; }; #endif // NET_GRPC_GATEWAY_EXAMPLES_ECHO_ECHO_SERVICE_IMPL_H_ ================================================ FILE: net/grpc/gateway/examples/echo/echoapp.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const echoapp = {}; /** * @param {Object} echoService * @param {Object} ctors */ echoapp.EchoApp = function(echoService, ctors) { this.echoService = echoService; this.ctors = ctors; }; echoapp.EchoApp.INTERVAL = 500; // ms echoapp.EchoApp.MAX_STREAM_MESSAGES = 50; /** * @param {string} message * @param {string} cssClass */ echoapp.EchoApp.addMessage = function(message, cssClass) { $("#first").after( $("
").addClass("row").append( $("

").append( $("").addClass("label " + cssClass).text(message)))); }; /** * @param {string} message */ echoapp.EchoApp.addLeftMessage = function(message) { this.addMessage(message, "label-primary pull-left"); }; /** * @param {string} message */ echoapp.EchoApp.addRightMessage = function(message) { this.addMessage(message, "label-default pull-right"); }; /** * @param {string} msg */ echoapp.EchoApp.prototype.echo = function(msg) { echoapp.EchoApp.addLeftMessage(msg); var unaryRequest = new this.ctors.EchoRequest(); unaryRequest.setMessage(msg); var call = this.echoService.echo(unaryRequest, {"custom-header-1": "value1"}, function(err, response) { if (err) { echoapp.EchoApp.addRightMessage('Error code: '+err.code+' "'+ err.message+'"'); } else { setTimeout(function () { echoapp.EchoApp.addRightMessage(response.getMessage()); }, echoapp.EchoApp.INTERVAL); } }); call.on('status', function(status) { if (status.metadata) { console.log("Received metadata"); console.log(status.metadata); } }); }; /** * @param {string} msg */ echoapp.EchoApp.prototype.echoError = function(msg) { echoapp.EchoApp.addLeftMessage(`Error: ${msg}`); var unaryRequest = new this.ctors.EchoRequest(); unaryRequest.setMessage(msg); this.echoService.echoAbort(unaryRequest, {}, function(err, response) { if (err) { echoapp.EchoApp.addRightMessage('Error code: '+err.code+' "'+ err.message+'"'); } }); }; /** * @param {string} msg * @param {number} count */ echoapp.EchoApp.prototype.repeatEcho = function(msg, count) { echoapp.EchoApp.addLeftMessage(msg); if (count > echoapp.EchoApp.MAX_STREAM_MESSAGES) { count = echoapp.EchoApp.MAX_STREAM_MESSAGES; } var streamRequest = new this.ctors.ServerStreamingEchoRequest(); streamRequest.setMessage(msg); streamRequest.setMessageCount(count); streamRequest.setMessageInterval(echoapp.EchoApp.INTERVAL); var stream = this.echoService.serverStreamingEcho( streamRequest, {"custom-header-1": "value1"}); stream.on('data', function(response) { echoapp.EchoApp.addRightMessage(response.getMessage()); }); stream.on('status', function(status) { if (status.metadata) { console.log("Received metadata"); console.log(status.metadata); } }); stream.on('error', function(err) { echoapp.EchoApp.addRightMessage('Error code: '+err.code+' "'+ err.message+'"'); }); stream.on('end', function() { console.log("stream end signal received"); }); }; /** * @param {Object} e event * @return {boolean} status */ echoapp.EchoApp.prototype.send = function(e) { var msg = $("#msg").val().trim(); $("#msg").val(''); // clear the text box if (!msg) return false; if (msg.indexOf(' ') > 0) { var count = msg.substr(0, msg.indexOf(' ')); if (/^\d+$/.test(count)) { this.repeatEcho(msg.substr(msg.indexOf(' ') + 1), count); } else if (count == 'err') { this.echoError(msg.substr(msg.indexOf(' ') + 1)); } else { this.echo(msg); } } else { this.echo(msg); } return false; }; /** * Load the app */ echoapp.EchoApp.prototype.load = function() { var self = this; $(document).ready(function() { // event handlers $("#send").click(self.send.bind(self)); $("#msg").keyup(function (e) { if (e.keyCode == 13) self.send(); // enter key return false; }); $("#msg").focus(); }); }; module.exports = echoapp; ================================================ FILE: net/grpc/gateway/examples/echo/echotest.html ================================================ Echo Example

Example: "Hello", "4 Hello", "err Hello"

================================================ FILE: net/grpc/gateway/examples/echo/envoy.yaml ================================================ admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: echo_service timeout: 0s max_stream_duration: grpc_timeout_header_max: 0s cors: allow_origin_string_match: - prefix: "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.filters.http.grpc_web typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - name: envoy.filters.http.cors typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: echo_service connect_timeout: 0.25s type: logical_dns # HTTP/2 support typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} lb_policy: round_robin load_assignment: cluster_name: cluster_0 endpoints: - lb_endpoints: - endpoint: address: socket_address: address: node-server port_value: 9090 ================================================ FILE: net/grpc/gateway/examples/echo/node-server/.gitignore ================================================ node_modules/ package-lock.json ================================================ FILE: net/grpc/gateway/examples/echo/node-server/package.json ================================================ { "name": "grpc-web-node-server-example", "version": "0.1.0", "main": "server.js", "dependencies": { "@grpc/grpc-js": "1.13.4", "@grpc/proto-loader": "~0.5.0", "async": "~3.2.3", "google-protobuf": "~3.21.4", "lodash": "~4.17.0" } } ================================================ FILE: net/grpc/gateway/examples/echo/node-server/server.js ================================================ /* * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ var PROTO_PATH = __dirname + '/../echo.proto'; var assert = require('assert'); var async = require('async'); var _ = require('lodash'); var grpc = require('@grpc/grpc-js'); var protoLoader = require('@grpc/proto-loader'); var packageDefinition = protoLoader.loadSync( PROTO_PATH, {keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }); var protoDescriptor = grpc.loadPackageDefinition(packageDefinition); var echo = protoDescriptor.grpc.gateway.testing; /** * @param {!Object} call * @return {!Object} metadata */ function copyMetadata(call) { var metadata = call.metadata.getMap(); var response_metadata = new grpc.Metadata(); for (var key in metadata) { response_metadata.set(key, metadata[key]); } return response_metadata; } /** * @param {!Object} call * @param {function():?} callback */ function doEcho(call, callback) { callback(null, { message: call.request.message }, copyMetadata(call)); } /** * @param {!Object} call * @param {function():?} callback */ function doEchoAbort(call, callback) { callback({ code: grpc.status.ABORTED, message: 'Aborted from server side.' }); } /** * @param {!Object} call */ function doServerStreamingEcho(call) { var senders = []; function sender(message, interval) { return (callback) => { call.write({ message: message }); _.delay(callback, interval); }; } for (var i = 0; i < call.request.message_count; i++) { senders[i] = sender(call.request.message, call.request.message_interval); } async.series(senders, () => { call.end(copyMetadata(call)); }); } /** * Get a new server with the handler functions in this file bound to the * methods it serves. * @return {!Server} The new server object */ function getServer() { var server = new grpc.Server(); server.addService(echo.EchoService.service, { echo: doEcho, echoAbort: doEchoAbort, serverStreamingEcho: doServerStreamingEcho, }); return server; } if (require.main === module) { var echoServer = getServer(); echoServer.bindAsync( '0.0.0.0:9090', grpc.ServerCredentials.createInsecure(), (err, port) => { assert.ifError(err); echoServer.start(); }); } exports.getServer = getServer; ================================================ FILE: net/grpc/gateway/examples/echo/package.json ================================================ { "name": "echo-closure-example", "version": "0.1.0", "description": "gRPC-Web Closure JS client example", "license": "Apache-2.0", "dependencies": {}, "devDependencies": { "google-closure-compiler": "~20200224.0.0", "google-closure-library": "~20210808.0.0" } } ================================================ FILE: net/grpc/gateway/examples/echo/ts-example/.gitignore ================================================ dist/ node_modules/ package-lock.json ================================================ FILE: net/grpc/gateway/examples/echo/ts-example/README.md ================================================ # Instructions to run the Typescript example ## Docker run ```bash # From root dir docker-compose up --build node-server envoy ts-client ``` Visit http://localhost:8081/echotest.html ## Manual run ### Step 1 - Run servers ```bash # From root dir docker-compose up --build node-server envoy ``` ### Step 2 - Codegen ```bash cd net/grpc/gateway/examples/echo ``` #### Option 1: `import_style=commonjs+dts` ``` RUN protoc -I=. echo.proto \ --js_out=import_style=commonjs:./ts-example \ --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:./ts-example ``` #### Option 2: `import_style=typescript` ``` RUN protoc -I=. echo.proto \ --js_out=import_style=commonjs:./ts-example \ --grpc-web_out=import_style=typescript,mode=grpcwebtext:./ts-example ``` ### Step 3 - (Optional) Update import style Change `client.ts` to use import style Option 2 if you had chosen `import_style=typescript` above: https://github.com/grpc/grpc-web/blob/60caece15489787662ebac6167572eecd5bfa568/net/grpc/gateway/examples/echo/ts-example/client.ts#L26-L27 ### Step 4 - Build JS files. ```bash cd net/grpc/gateway/examples/echo/ts-example npm install npx tsc mv *_pb.js dist/ npx webpack ``` ### Step 5 - Host and visit page ```bash # In the ./ts-example folder python3 -m http.server 8081 ``` Visit http://localhost:8081/echotest.html ================================================ FILE: net/grpc/gateway/examples/echo/ts-example/client.ts ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import * as grpcWeb from 'grpc-web'; import * as $ from 'jquery'; // Uncomment either one of the following: // Option 1: import_style=commonjs+dts import {EchoServiceClient} from './echo_grpc_web_pb'; // Option 2: import_style=typescript // import {EchoServiceClient} from './EchoServiceClientPb'; import {EchoRequest, EchoResponse, ServerStreamingEchoRequest, ServerStreamingEchoResponse} from './echo_pb'; class EchoApp { static readonly INTERVAL = 500; // ms static readonly MAX_STREAM_MESSAGES = 50; stream?: grpcWeb.ClientReadableStream; constructor(public echoService: EchoServiceClient) {} static addMessage(message: string, cssClass: string) { $('#first').after($('
').addClass('row').append($('

').append( $('').addClass('label ' + cssClass).text(message)))); } static addLeftMessage(message: string) { this.addMessage(message, 'label-primary pull-left'); } static addRightMessage(message: string) { this.addMessage(message, 'label-default pull-right'); } echo(msg: string) { EchoApp.addLeftMessage(msg); const request = new EchoRequest(); request.setMessage(msg); const call = this.echoService.echo( request, {'custom-header-1': 'value1'}, (err: grpcWeb.RpcError, response: EchoResponse) => { if (err) { if (err.code !== grpcWeb.StatusCode.OK) { EchoApp.addRightMessage( 'Error code: ' + err.code + ' "' + err.message + '"'); } } else { setTimeout(() => { EchoApp.addRightMessage(response.getMessage()); }, EchoApp.INTERVAL); } }); call.on('status', (status: grpcWeb.Status) => { if (status.metadata) { console.log('Received metadata'); console.log(status.metadata); } }); } echoError(msg: string) { EchoApp.addLeftMessage(`Error: ${msg}`); const request = new EchoRequest(); request.setMessage(msg); this.echoService.echoAbort( request, {}, (err: grpcWeb.RpcError, response: EchoResponse) => { if (err && err.code !== grpcWeb.StatusCode.OK) { EchoApp.addRightMessage( 'Error code: ' + err.code + ' "' + decodeURI(err.message) + '"'); } }); } cancel() { EchoApp.addLeftMessage('Cancel'); if (this.stream) { this.stream.cancel(); } } repeatEcho(msg: string, count: number) { EchoApp.addLeftMessage(msg); if (count > EchoApp.MAX_STREAM_MESSAGES) { count = EchoApp.MAX_STREAM_MESSAGES; } const request = new ServerStreamingEchoRequest(); request.setMessage(msg); request.setMessageCount(count); request.setMessageInterval(EchoApp.INTERVAL); this.stream = this.echoService.serverStreamingEcho( request, {'custom-header-1': 'value1'}); const self = this; this.stream.on('data', (response: ServerStreamingEchoResponse) => { EchoApp.addRightMessage(response.getMessage()); }); this.stream.on('status', (status: grpcWeb.Status) => { if (status.metadata) { console.log('Received metadata'); console.log(status.metadata); } }); this.stream.on('error', (err: grpcWeb.RpcError) => { EchoApp.addRightMessage( 'Error code: ' + err.code + ' "' + err.message + '"'); }); this.stream.on('end', () => { console.log('stream end signal received'); }); } send(e: {}) { const _msg: string = $('#msg').val() as string; const msg = _msg.trim(); $('#msg').val(''); // clear the text box if (!msg) return false; if (msg.indexOf(' ') > 0) { const count = msg.substr(0, msg.indexOf(' ')); if (/^\d+$/.test(count)) { this.repeatEcho(msg.substr(msg.indexOf(' ') + 1), Number(count)); } else if (count === 'err') { this.echoError(msg.substr(msg.indexOf(' ') + 1)); } else { this.echo(msg); } } else if (msg === 'cancel') { this.cancel(); } else { this.echo(msg); } } load() { const self = this; $(document).ready(() => { // event handlers $('#send').click(self.send.bind(self)); $('#msg').keyup((e) => { if (e.keyCode === 13) self.send(e); // enter key return false; }); $('#msg').focus(); }); } } const echoService = new EchoServiceClient('http://localhost:8080', null, null); const echoApp = new EchoApp(echoService); echoApp.load(); ================================================ FILE: net/grpc/gateway/examples/echo/ts-example/echotest.html ================================================ Echo Example

Example: "Hello", "4 Hello", "err Hello"

================================================ FILE: net/grpc/gateway/examples/echo/ts-example/package.json ================================================ { "devDependencies": { "@types/google-protobuf": "~3.15.12", "@types/jquery": "~3.3.6", "@types/node": "latest", "google-protobuf": "~3.21.4", "grpc-web": "~2.0.2", "jquery": "~3.5.1", "mock-xmlhttprequest": "~2.0.0", "typescript": "latest", "webpack": "5.101.3", "webpack-cli": "~5.1.1" } } ================================================ FILE: net/grpc/gateway/examples/echo/ts-example/tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "module": "commonjs", "strict": true, "allowJs": true, "outDir": "./dist", "types": ["node"], }, "include": [ "client.ts", "echo_pb.js", "echo_pb.d.ts", "echo_grpc_web_pb.js", "echo_grpc_web_pb.d.ts", "EchoServiceClientPb.ts" ], "exclude": [ "node_modules" ] } ================================================ FILE: net/grpc/gateway/examples/echo/ts-example/webpack.config.js ================================================ module.exports = { mode: 'production', entry: './dist/client.js', }; ================================================ FILE: net/grpc/gateway/examples/echo/tutorial.md ================================================ This tutorial provides a detailed guide on how to run a gRPC service and access it in the browser. ## Define the Service The first step when creating a gRPC service is to define the service methods and their request and response message types using protocol buffers. In this example, we define our `EchoService` in a file called [`echo.proto`](echo.proto). For more information about protocol buffers and proto3 syntax, please see the [protobuf documentation][]. ```protobuf message EchoRequest { string message = 1; } message EchoResponse { string message = 1; } service EchoService { rpc Echo(EchoRequest) returns (EchoResponse); } ``` ## Implement gRPC Backend Server Next, we implement our EchoService interface using Node in the backend gRPC `EchoServer`. This will handle requests from clients. See the file [`node-server/server.js`](./node-server/server.js) for details. You can implement the server in any language supported by gRPC. Please see the [gRPC website][] for more details. ```js function doEcho(call, callback) { callback(null, {message: call.request.message}); } ``` ## Configure the Envoy Proxy In this example, we will use the [Envoy](https://www.envoyproxy.io/) proxy to forward the gRPC browser request to the backend server. You can see the complete config file in [envoy.yaml](./envoy.yaml) To forward the gRPC requests to the backend server, we need a block like this: ```yaml static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: echo_service timeout: 0s http_filters: - name: envoy.filters.http.grpc_web typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: echo_service connect_timeout: 0.25s type: logical_dns # HTTP/2 support typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} lb_policy: round_robin load_assignment: cluster_name: cluster_0 endpoints: - lb_endpoints: - endpoint: address: socket_address: address: node-server port_value: 9090 ``` You may also need to add some CORS setup to make sure the browser can request cross-origin content. In this simple example, the browser makes gRPC requests to port `:8080`. Envoy forwards the request to the backend gRPC server listening on port `:9090`. ## Generate Protobuf Messages and Service Client Stub To generate the protobuf message classes from our `echo.proto`, run the following command: ```sh $ protoc -I=$DIR echo.proto \ --js_out=import_style=commonjs:$OUT_DIR ``` The `import_style` option passed to the `--js_out` flag makes sure the generated files will have CommonJS style `require()` statements. To generate the gRPC-Web service client stub, first you need the gRPC-Web protoc plugin. To compile the plugin `protoc-gen-grpc-web`, you need to run this from the repo's root directory: ```sh $ cd grpc-web $ sudo make install-plugin ``` To generate the service client stub file, run this command: ```sh $ protoc -I=$DIR echo.proto \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:$OUT_DIR ``` In the `--grpc-web_out` param above: - `mode` can be `grpcwebtext` (default) or `grpcweb` - `import_style` can be `closure` (default) or `commonjs` Our command generates the client stub, by default, to the file `echo_grpc_web_pb.js`. ## Write JS Client Code Now you are ready to write some JS client code. Put this in a `client.js` file. ```js const {EchoRequest, EchoResponse} = require('./echo_pb.js'); const {EchoServiceClient} = require('./echo_grpc_web_pb.js'); var echoService = new EchoServiceClient('http://localhost:8080'); var request = new EchoRequest(); request.setMessage('Hello World!'); echoService.echo(request, {}, function(err, response) { // ... }); ``` You will need a `package.json` file ```json { "name": "grpc-web-commonjs-example", "dependencies": { "google-protobuf": "~3.21.4", "grpc-web": "~2.0.2" }, "devDependencies": { "webpack": "~5.82.1", "webpack-cli": "~5.1.1" } } ``` ## Compile the JS Library Finally, putting all these together, we can compile all the relevant JS files into one single JS library that can be used in the browser. ```sh $ npm install $ npx webpack ./client.js ``` Now embed `dist/main.js` into your project and see it in action! [protobuf documentation]:https://developers.google.com/protocol-buffers/ [gRPC website]:https://grpc.io ================================================ FILE: net/grpc/gateway/examples/helloworld/.gitignore ================================================ dist/ *_pb.js node_modules/ package-lock.json ================================================ FILE: net/grpc/gateway/examples/helloworld/README.md ================================================ # gRPC-Web Hello World Guide This guide is intended to help you get started with gRPC-Web with a simple Hello World example. For more information about the gRPC-Web project as a whole, please visit the [main repo](https://github.com/grpc/grpc-web). All the code for this example can be found in this current directory. ```sh $ cd net/grpc/gateway/examples/helloworld ``` ## Define the Service First, let's define a gRPC service using [protocol buffers](https://developers.google.com/protocol-buffers/). Put this in the `helloworld.proto` file. Here we define a request message, a response message, and a service with one RPC method: `SayHello`. ```protobuf syntax = "proto3"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } ``` ## Implement the Service Then, we need to implement the gRPC Service. In this example, we will use NodeJS. Put this in a `server.js` file. Here, we receive the client request, and we can access the message field via `call.request.name`. Then we construct a nice response and send it back to the client via `callback(null, response)`. ```js var PROTO_PATH = __dirname + '/helloworld.proto'; var assert = require('assert'); var grpc = require('@grpc/grpc-js'); var protoLoader = require('@grpc/proto-loader'); var packageDefinition = protoLoader.loadSync( PROTO_PATH, {keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }); var protoDescriptor = grpc.loadPackageDefinition(packageDefinition); var helloworld = protoDescriptor.helloworld; function doSayHello(call, callback) { callback(null, { message: 'Hello! ' + call.request.name }); } function getServer() { var server = new grpc.Server(); server.addService(helloworld.Greeter.service, { sayHello: doSayHello, }); return server; } if (require.main === module) { var server = getServer(); server.bindAsync( '0.0.0.0:9090', grpc.ServerCredentials.createInsecure(), (err, port) => { assert.ifError(err); server.start(); }); } exports.getServer = getServer; ``` ## Configure the Proxy Next up, we need to configure the Envoy proxy to forward the browser's gRPC-Web requests to the backend. Put this in an `envoy.yaml` file. Here we configure Envoy to listen at port `:8080`, and forward any gRPC-Web requests to a cluster at port `:9090`. ```yaml static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: greeter_service max_stream_duration: grpc_timeout_header_max: 0s cors: allow_origin_string_match: - prefix: "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.filters.http.grpc_web typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - name: envoy.filters.http.cors typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: greeter_service connect_timeout: 0.25s type: logical_dns # HTTP/2 support typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} lb_policy: round_robin load_assignment: cluster_name: cluster_0 endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 0.0.0.0 port_value: 9090 ``` > NOTE: As per [this issue](https://github.com/grpc/grpc-web/issues/436): if > you are running Docker on Mac/Windows, change the last `address: 0.0.0.0` to > > ```yaml > ... > socket_address: > address: host.docker.internal > ``` > > or if your version of Docker on Mac is older than v18.03.0, change it to: > > ```yaml > ... > socket_address: > address: docker.for.mac.localhost > ``` > NOTE: As per > [Envoy - Envoy Networking](ttps://www.envoyproxy.io/docs/envoy/latest/start/quick-start/run-envoy#envoy-networking), > if your environment does not enable IPv6, enable only IPv4 in the cluster > by adding the `dns_lookup_family: V4_ONLY` field: > > ```yaml > ... > clusters: > - name: greeter_service > dns_lookup_family: V4_ONLY > ``` ## Write Client Code Now, we are ready to write some client code! Put this in a `client.js` file. ```js const {HelloRequest, HelloReply} = require('./helloworld_pb.js'); const {GreeterClient} = require('./helloworld_grpc_web_pb.js'); var client = new GreeterClient('http://localhost:8080'); var request = new HelloRequest(); request.setName('World'); client.sayHello(request, {}, (err, response) => { console.log(response.getMessage()); }); ``` The classes `HelloRequest`, `HelloReply` and `GreeterClient` we import here are generated for you by the `protoc` generator utility (which we will cover in the next section) from the `helloworld.proto` file we defined earlier. Then we instantiate a `GreeterClient` instance, set the field in the `HelloRequest` protobuf object, and we can make a gRPC call via `client.sayHello()`, just like how we defined in the `helloworld.proto` file. You will need a `package.json` file. This is needed for both the `server.js` and the `client.js` files. ```json { "name": "grpc-web-simple-example", "version": "0.1.0", "description": "gRPC-Web simple example", "main": "server.js", "devDependencies": { "@grpc/grpc-js": "~1.0.5", "@grpc/proto-loader": "~0.5.4", "async": "~1.5.2", "google-protobuf": "~3.21.4", "grpc-web": "~2.0.2", "lodash": "~4.17.0", "webpack": "~5.82.1", "webpack-cli": "~5.1.1" } } ``` And finally a simple `index.html` file. ```html gRPC-Web Example

Open up the developer console and see the logs for the output.

``` The `./dist/main.js` file will be generated by `webpack` (which will be covered in the next section). And that's it! We have all the code ready. Let's run the example! ## Generate Protobuf Messages and Client Service Stub ### Install Plugins To generate the protobuf messages and client service stub class from your `.proto` definitions, we need: - the `protoc` binary, _and_ - the `protoc-gen-js` binary, _and_ - the `protoc-gen-grpc-web` plugin. Follow the instructions [here](https://github.com/grpc/grpc-web?tab=readme-ov-file#code-generator-plugins) if you don't have them installed already. ### Generating stubs When you have both `protoc`, `protoc-gen-js`, and `protoc-gen-grpc-web` installed, you can now run this command: ```sh $ protoc -I=. helloworld.proto \ --js_out=import_style=commonjs:. \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:. ``` After the command runs successfully, you should now see two new files generated in the current directory: - `helloworld_pb.js`: this contains the `HelloRequest` and `HelloReply` classes - `helloworld_grpc_web_pb.js`: this contains the `GreeterClient` class These are also the 2 files that our `client.js` file imported earlier in the example. ## Compile the Client JavaScript Code Next, we need to compile the client side JavaScript code into something that can be consumed by the browser. ```sh $ npm install $ npx webpack ./client.js ``` Here we use `webpack` and give it an entry point `client.js`. You can also use `browserify` or other similar tools. This will resolve all the `require()` statements and produce a `./dist/main.js` file that can be embedded in our `index.html` file. ## Run the Example! We are ready to run the Hello World example. The following set of commands will run the 3 processes all in the background. 1. Run the NodeJS gRPC Service. This listens at port `:9090`. ```sh $ node server.js & ``` 2. Run the Envoy proxy. The `envoy.yaml` file configures Envoy to listen to browser requests at port `:8080`, and forward them to port `:9090` (see above). ```sh $ docker run -d -v "$(pwd)"/envoy.yaml:/etc/envoy/envoy.yaml:ro \ --network=host envoyproxy/envoy:v1.22.0 ``` > NOTE: As per [this issue](https://github.com/grpc/grpc-web/issues/436): > if you are running Docker on Mac/Windows, remove the `--network=host` option: > > ```sh > $ docker run -d -v "$(pwd)"/envoy.yaml:/etc/envoy/envoy.yaml:ro \ > -p 8080:8080 -p 9901:9901 envoyproxy/envoy:v1.22.0 > ``` 3. Run the simple Web Server. This hosts the static file `index.html` and `dist/main.js` we generated earlier. ```sh $ python3 -m http.server 8081 & ``` When these are all ready, you can open a browser tab and navigate to ``` localhost:8081 ``` Open up the developer console and you should see the following printed out: ``` Hello! World ``` ================================================ FILE: net/grpc/gateway/examples/helloworld/client.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const {HelloRequest, RepeatHelloRequest, HelloReply} = require('./helloworld_pb.js'); const {GreeterClient} = require('./helloworld_grpc_web_pb.js'); var client = new GreeterClient('http://' + window.location.hostname + ':8080', null, null); // simple unary call var request = new HelloRequest(); request.setName('World'); client.sayHello(request, {}, (err, response) => { if (err) { console.log(`Unexpected error for sayHello: code = ${err.code}` + `, message = "${err.message}"`); } else { console.log(response.getMessage()); } }); // server streaming call var streamRequest = new RepeatHelloRequest(); streamRequest.setName('World'); streamRequest.setCount(5); var stream = client.sayRepeatHello(streamRequest, {}); stream.on('data', (response) => { console.log(response.getMessage()); }); stream.on('error', (err) => { console.log(`Unexpected stream error: code = ${err.code}` + `, message = "${err.message}"`); }); ================================================ FILE: net/grpc/gateway/examples/helloworld/debugging/node-client.js ================================================ // NOTE: This client is used for debugging the node gRPC server WITHOUT the // Envoy proxy. It does not use the gRPC-Web protocol. var PROTO_PATH = __dirname + '/helloworld.proto'; var async = require('async'); var grpc = require('@grpc/grpc-js'); var protoLoader = require('@grpc/proto-loader'); var packageDefinition = protoLoader.loadSync( PROTO_PATH, {keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }); var protoDescriptor = grpc.loadPackageDefinition(packageDefinition); var helloworld = protoDescriptor.helloworld; var client = new helloworld.Greeter('localhost:9090', grpc.credentials.createInsecure()); /** * @param {function():?} callback */ function runSayHello(callback) { client.sayHello({name: 'John'}, {}, (err, response) => { console.log(response.message); callback(); }); } /** * @param {function():?} callback */ function runSayRepeatHello(callback) { var stream = client.sayRepeatHello({name: 'John', count: 5}, {}); stream.on('data', (response) => { console.log(response.message); }); stream.on('end', () => { callback(); }); } /** * Run all of the demos in order */ function main() { async.series([ runSayHello, runSayRepeatHello, ]); } if (require.main === module) { main(); } ================================================ FILE: net/grpc/gateway/examples/helloworld/envoy.yaml ================================================ admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: greeter_service timeout: 0s max_stream_duration: grpc_timeout_header_max: 0s cors: allow_origin_string_match: - prefix: "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.filters.http.grpc_web typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - name: envoy.filters.http.cors typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: greeter_service connect_timeout: 0.25s type: logical_dns # HTTP/2 support typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} lb_policy: round_robin # win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below load_assignment: cluster_name: cluster_0 endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 0.0.0.0 port_value: 9090 ================================================ FILE: net/grpc/gateway/examples/helloworld/helloworld.proto ================================================ // Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package helloworld; service Greeter { // unary call rpc SayHello(HelloRequest) returns (HelloReply); // server streaming call rpc SayRepeatHello(RepeatHelloRequest) returns (stream HelloReply); } message HelloRequest { string name = 1; } message RepeatHelloRequest { string name = 1; int32 count = 2; } message HelloReply { string message = 1; } ================================================ FILE: net/grpc/gateway/examples/helloworld/index.html ================================================ gRPC-Web Example

Open up the developer console and see the logs for the output.

================================================ FILE: net/grpc/gateway/examples/helloworld/package.json ================================================ { "name": "grpc-web-simple-example", "version": "0.1.0", "description": "gRPC-Web simple example", "main": "server.js", "devDependencies": { "@grpc/grpc-js": "1.13.4", "@grpc/proto-loader": "~0.5.4", "async": "~3.2.3", "google-protobuf": "~3.21.4", "grpc-web": "~2.0.2", "lodash": "~4.17.0", "webpack": "5.101.3", "webpack-cli": "~5.1.1" } } ================================================ FILE: net/grpc/gateway/examples/helloworld/server.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ var PROTO_PATH = __dirname + '/helloworld.proto'; var assert = require('assert'); var async = require('async'); var _ = require('lodash'); var grpc = require('@grpc/grpc-js'); var protoLoader = require('@grpc/proto-loader'); var packageDefinition = protoLoader.loadSync( PROTO_PATH, {keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }); var protoDescriptor = grpc.loadPackageDefinition(packageDefinition); var helloworld = protoDescriptor.helloworld; /** * @param {!Object} call * @param {function():?} callback */ function doSayHello(call, callback) { callback(null, {message: 'Hello! '+ call.request.name}); } /** * @param {!Object} call */ function doSayRepeatHello(call) { var senders = []; function sender(name) { return (callback) => { call.write({ message: 'Hey! ' + name }); _.delay(callback, 500); // in ms }; } for (var i = 0; i < call.request.count; i++) { senders[i] = sender(call.request.name + i); } async.series(senders, () => { call.end(); }); } /** * @return {!Object} gRPC server */ function getServer() { var server = new grpc.Server(); server.addService(helloworld.Greeter.service, { sayHello: doSayHello, sayRepeatHello: doSayRepeatHello, }); return server; } if (require.main === module) { var server = getServer(); server.bindAsync( '0.0.0.0:9090', grpc.ServerCredentials.createInsecure(), (err, port) => { assert.ifError(err); server.start(); }); } exports.getServer = getServer; ================================================ FILE: packages/grpc-web/.gitignore ================================================ /index.js /generated/ package-lock.json node_modules/ __pycache__/ ================================================ FILE: packages/grpc-web/.npmignore ================================================ /scripts /package-lock.json /.gitignore ================================================ FILE: packages/grpc-web/README.md ================================================ # gRPC-Web Client Runtime Library gRPC-Web provides a Javascript library that lets browser clients access a gRPC service. You can find out much more about gRPC in its own [website](https://grpc.io). gRPC-Web is now Generally Available, and considered stable enough for production use. gRPC-Web clients connect to gRPC services via a special gateway proxy: the current version of the library uses [Envoy](https://www.envoyproxy.io/) by default, in which gRPC-Web support is built-in. In the future, we expect gRPC-Web to be supported in language-specific Web frameworks, such as Python, Java, and Node. See the [roadmap](https://github.com/grpc/grpc-web/blob/master/doc/roadmap.md) doc. ## Quick Start This example is using the `echo.proto` file from the [Echo Example](https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/echo). 1. Add `grpc-web` as a dependency using `npm`. ```sh $ npm i grpc-web ``` 2. Download `protoc` and the `protoc-gen-grpc-web` protoc plugin. You can download the `protoc` binary from the official [protocolbuffers](https://github.com/protocolbuffers/protobuf/releases) release page. You can download the `protoc-gen-grpc-web` protoc plugin from our Github [release](https://github.com/grpc/grpc-web/releases) page. Make sure they are both executable and are discoverable from your PATH. 3. Generate your proto messages and the service client stub classes with `protoc` and the `protoc-gen-grpc-web` plugin. You can set the `import_style=commonjs` option for both `--js_out` and `--grpc-web_out`. ```sh $ protoc -I=$DIR echo.proto \ --js_out=import_style=commonjs:generated \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:generated ``` 4. Start using your generated client! ```js const {EchoServiceClient} = require('./generated/echo_grpc_web_pb.js'); const {EchoRequest} = require('./generated/echo_pb.js'); const client = new EchoServiceClient('localhost:8080'); const request = new EchoRequest(); request.setMessage('Hello World!'); const metadata = {'custom-header-1': 'value1'}; client.echo(request, metadata, (err, response) => { // ... }); ``` ## What's Next To complete the example, you need to run a proxy that understands the [gRPC-Web protocol](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2) between your browser client and your gRPC service. The default proxy currently is [Envoy](https://www.envoyproxy.io/). Please visit our [Github repo](https://github.com/grpc/grpc-web) for more information. Here's a quick way to get started! ```sh $ git clone https://github.com/grpc/grpc-web $ cd grpc-web $ docker-compose up node-server envoy commonjs-client ``` Open a browser tab, and go to: ``` http://localhost:8081/echotest.html ``` ## TypeScript Support The `grpc-web` module can now be imported as a TypeScript module. This is currently an experimental feature. Any feedback welcome! When using the `protoc-gen-grpc-web` protoc plugin, mentioned above, pass in either: - `import_style=commonjs+dts`: existing CommonJS style stub + `.d.ts` typings - `import_style=typescript`: full TypeScript output ```ts import * as grpcWeb from 'grpc-web'; import {EchoServiceClient} from './echo_grpc_web_pb'; import {EchoRequest, EchoResponse} from './echo_pb'; const echoService = new EchoServiceClient('http://localhost:8080', null, null); const request = new EchoRequest(); request.setMessage('Hello World!'); const call = echoService.echo(request, {'custom-header-1': 'value1'}, (err: grpcWeb.RpcError, response: EchoResponse) => { console.log(response.getMessage()); }); call.on('status', (status: grpcWeb.Status) => { // ... }); ``` See a full TypeScript example [here](https://github.com/grpc/grpc-web/blob/master/net/grpc/gateway/examples/echo/ts-example/client.ts). ## Run Tests Pre-requisites: - `protoc` - `protoc-gen-grpc-web` plugin ```sh $ npm test ``` ================================================ FILE: packages/grpc-web/docker/jsunit-test/Dockerfile ================================================ FROM selenium/standalone-chrome:112.0.5615.165 ARG NODE_VERSION=20.0.0 USER root # Install Node.js dependencies RUN apt-get update && apt-get install -y nodejs npm curl # Install nvm and set up Node.js RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash ENV NVM_DIR=$HOME/.nvm RUN . $NVM_DIR/nvm.sh && \ nvm install $NODE_VERSION && \ nvm alias default $NODE_VERSION && \ nvm use default ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH WORKDIR /grpc-web # Copy all needed files before running npm install or compiler COPY ./packages ./packages COPY ./javascript ./javascript COPY ./scripts ./scripts # Install npm dependencies and build the JS library WORKDIR /grpc-web/packages/grpc-web RUN npm install ================================================ FILE: packages/grpc-web/exports.js ================================================ /** * @fileoverview Export symbols needed by generated code in CommonJS style. * * Note that public methods called by generated code are exposed * using Closure Compiler's @export annotation */ goog.module('grpc.web.Exports'); const CallOptions = goog.require('grpc.web.CallOptions'); const MethodDescriptor = goog.require('grpc.web.MethodDescriptor'); const GrpcWebClientBase = goog.require('grpc.web.GrpcWebClientBase'); const RpcError = goog.require('grpc.web.RpcError'); const StatusCode = goog.require('grpc.web.StatusCode'); const MethodType = goog.require('grpc.web.MethodType'); module['exports']['CallOptions'] = CallOptions; module['exports']['MethodDescriptor'] = MethodDescriptor; module['exports']['GrpcWebClientBase'] = GrpcWebClientBase; module['exports']['RpcError'] = RpcError; module['exports']['StatusCode'] = StatusCode; module['exports']['MethodType'] = MethodType; // Temporary hack to fix https://github.com/grpc/grpc-web/issues/1153, which is // caused by `goog.global` not pointing to the global scope when grpc-web is // being imported as a CommonJS module. // TODO: Remove this hack after `goog.global` is fixed. goog.Timer.defaultTimerObject = (typeof globalThis !== "undefined" && globalThis) || self; ================================================ FILE: packages/grpc-web/externs.js ================================================ var module; /** * List of functions we want to preserve when running the closure compiler * with --compilation_level=ADVANCED_OPTIMIZATIONS. */ module.ClientReadableStream = function() {}; module.ClientReadableStream.prototype.on = function(eventType, callback) {}; module.ClientReadableStream.prototype.removeListener = function(eventType, callback) {}; module.ClientReadableStream.prototype.cancel = function() {}; module.GenericClient = function() {}; module.GenericClient.prototype.unaryCall = function(request) {}; module.GenericClient.prototype.call = function(requestMessage, methodDescriptor) {}; module.UnaryInterceptor = function() {}; module.UnaryInterceptor.prototype.intercept = function(request, invoker) {}; module.StreamInterceptor = function() {}; module.StreamInterceptor.prototype.intercept = function(request, invoker) {}; module.Request = function() {}; module.Request.prototype.getRequestMessage = function() {}; module.Request.prototype.getMethodDescriptor = function() {}; module.Request.prototype.getMetadata = function() {}; module.Request.prototype.getCallOptions = function() {}; module.UnaryResponse = function() {}; module.UnaryResponse.prototype.getResponseMessage = function() {}; module.UnaryResponse.prototype.getMetadata = function() {}; module.UnaryResponse.prototype.getMethodDescriptor = function() {}; module.UnaryResponse.prototype.getStatus = function() {}; module.MethodDescriptor = function() {}; module.MethodDescriptor.getName = function() {}; ================================================ FILE: packages/grpc-web/gulpfile.js ================================================ const connect = require('gulp-connect'); const gulp = require('gulp'); gulp.task('serve', () => { connect.server({ // Serves the root of github repo so tests an access javascript files. root: '../../', port: 4000 }); }); ================================================ FILE: packages/grpc-web/index.d.ts ================================================ declare module "grpc-web" { export interface Metadata { [s: string]: string; } export class AbstractClientBase { thenableCall ( method: string, request: REQ, metadata: Metadata, methodDescriptor: MethodDescriptor, options?: PromiseCallOptions ): Promise; unaryCall( method: string, request: REQ, metadata: Metadata, methodDescriptor: MethodDescriptor ): Promise; rpcCall ( method: string, request: REQ, metadata: Metadata, methodDescriptor: MethodDescriptor, callback: (err: RpcError, response: RESP) => void ): ClientReadableStream; serverStreaming ( method: string, request: REQ, metadata: Metadata, methodDescriptor: MethodDescriptor ): ClientReadableStream; } export class ClientReadableStream { on (eventType: "error", callback: (err: RpcError) => void): ClientReadableStream; on (eventType: "status", callback: (status: Status) => void): ClientReadableStream; on (eventType: "metadata", callback: (status: Metadata) => void): ClientReadableStream; on (eventType: "data", callback: (response: RESP) => void): ClientReadableStream; on (eventType: "end", callback: () => void): ClientReadableStream; removeListener (eventType: "error", callback: (err: RpcError) => void): void; removeListener (eventType: "status", callback: (status: Status) => void): void; removeListener (eventType: "metadata", callback: (status: Metadata) => void): void; removeListener (eventType: "data", callback: (response: RESP) => void): void; removeListener (eventType: "end", callback: () => void): void; cancel (): void; } export interface StreamInterceptor { intercept(request: Request, invoker: (request: Request) => ClientReadableStream): ClientReadableStream; } export interface UnaryInterceptor { intercept(request: Request, invoker: (request: Request) => Promise>): Promise>; } /** Options for gRPC-Web calls returning a Promise. */ export interface PromiseCallOptions { /** An AbortSignal to abort the call. */ readonly signal?: AbortSignal; } export class MethodDescriptor { constructor(name: string, methodType: string, requestType: new (...args: unknown[]) => REQ, responseType: new (...args: unknown[]) => RESP, requestSerializeFn: any, responseDeserializeFn: any); getName(): string; } export class Request { getRequestMessage(): REQ; getMethodDescriptor(): MethodDescriptor; getMetadata(): Metadata; } export class UnaryResponse { getResponseMessage(): RESP; getMetadata(): Metadata; getMethodDescriptor(): MethodDescriptor; getStatus(): Status; } export interface GrpcWebClientBaseOptions { format?: string; suppressCorsPreflight?: boolean; withCredentials?: boolean; unaryInterceptors?: UnaryInterceptor[]; streamInterceptors?: StreamInterceptor[]; } export class GrpcWebClientBase extends AbstractClientBase { constructor(options?: GrpcWebClientBaseOptions); } export class RpcError extends Error { constructor(code: StatusCode, message: string, metadata: Metadata); code: StatusCode; metadata: Metadata; } export interface Status { code: number; details: string; metadata?: Metadata; } export enum StatusCode { OK, CANCELLED, UNKNOWN, INVALID_ARGUMENT, DEADLINE_EXCEEDED, NOT_FOUND, ALREADY_EXISTS, PERMISSION_DENIED, RESOURCE_EXHAUSTED, FAILED_PRECONDITION, ABORTED, OUT_OF_RANGE, UNIMPLEMENTED, INTERNAL, UNAVAILABLE, DATA_LOSS, UNAUTHENTICATED, } export namespace MethodType { const UNARY: string; const SERVER_STREAMING: string; } } ================================================ FILE: packages/grpc-web/package.json ================================================ { "name": "grpc-web", "version": "2.0.2", "author": "Google Inc.", "description": "gRPC-Web Client Runtime Library", "homepage": "https://grpc.io/", "repository": { "type": "git", "url": "https://github.com/grpc/grpc-web.git" }, "bugs": "https://github.com/grpc/grpc-web/issues", "files": [ "index.js", "index.d.ts" ], "main": "index.js", "typings": "index.d.ts", "scripts": { "build": "node scripts/build.js", "prepare": "npm run build", "prepublishOnly": "npm run build", "test": "npm run test-jsunit && npm run test-mocha", "test-mocha": "mocha --timeout 10000 \"./test/**/*_test.js\"", "test-jsunit": "./scripts/generate_test_files.sh && ./scripts/run_jsunit_tests.sh && rm -rf ./generated" }, "license": "Apache-2.0", "devDependencies": { "@types/google-protobuf": "~3.15.12", "command-exists": "~1.2.8", "google-closure-compiler": "^20250820.0.0", "google-closure-deps": "~20210601.0.0", "google-closure-library": "^20230802.0.0", "google-protobuf": "~3.21.4", "gulp": "~4.0.2", "gulp-connect": "~5.7.0", "gulp-eval": "~1.0.0", "mocha": "~5.2.0", "mock-xmlhttprequest": "~2.0.0", "protractor": "~7.0.0", "typescript": "latest" } } ================================================ FILE: packages/grpc-web/protractor.conf.js ================================================ /** * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Stores the configuration of Protractor. It is loaded by protractor to run * tests. * * Intended to be used through ./run_jsunit_test.sh */ // Common configuration. config = { // Using jasmine to wrap Closure JSUnit tests. framework: 'jasmine', // The jasmine specs to run. specs: ['protractor_spec.js'], // Jasmine options. Increase the timeout to 5min instead of the default 30s. jasmineNodeOpts: { // Default time to wait in ms before a test fails. defaultTimeoutInterval: 5 * 60 * 1000 // 5 minutes } }; // Configuration for headless chrome. config.directConnect = true; config.multiCapabilities = [{ browserName: 'chrome', chromeOptions: { args: [ "--headless", "--disable-gpu", "--window-size=800,600", "--no-sandbox", "--disable-dev-shm-usage" ] } }]; exports.config = config; ================================================ FILE: packages/grpc-web/protractor_spec.js ================================================ /** * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var allTests = require('./generated/all_tests'); var TEST_SERVER = 'http://localhost:4000'; describe('Run all Closure unit tests', function() { /** * Waits for current tests to be executed. * @param {function(!Object)} done The function called when the test is finished. * @param {function(!Error)} fail The function called when an unrecoverable error * happened during the test. */ var waitForTest = function(done, fail) { // executeScript runs the passed method in the "window" context of // the current test. JSUnit exposes hooks into the test's status through // the "G_testRunner" global object. browser.executeScript(function() { if (window['G_testRunner'] && window['G_testRunner']['isFinished']()) { return { isFinished: true, isSuccess: window['G_testRunner']['isSuccess'](), report: window['G_testRunner']['getReport']() }; } else { return {'isFinished': false}; } }).then(function(status) { if (status && status.isFinished) { done(status); } else { waitForTest(done, fail); } }, function(err) { // This can happen if the webdriver had an issue executing the script. fail(err); }); }; /** * Executes the test cases for the file at the given testPath. * @param {!string} testPath The path of the current test suite to execute. */ var executeTest = function(testPath) { it('runs ' + testPath + ' with success', function(done) { /** * Runs the test routines for a given test path. * @param {function()} done The function to run on completion. */ var runRoutine = function(done) { browser.navigate() .to(TEST_SERVER + '/' + testPath) .then(function() { waitForTest(function(status) { expect(status).toBeSuccess(); done(); }, function(err) { done.fail(err); }); }, function(err) { done.fail(err); }); }; // Run the test routine. runRoutine(done); }); }; beforeEach(function() { jasmine.addMatchers({ // This custom matcher allows for cleaner reports. toBeSuccess: function() { return { // Checks that the status report is successful, otherwise displays // the report as error message. compare: function(status) { return { pass: status.isSuccess, message: 'Some test cases failed!\n\n' + status.report }; } }; } }); }); if (!allTests.length) { throw new Error('Cannot find any JsUnit tests!!'); } // Run all tests. for (var i = 0; i < allTests.length; i++) { var testPath = allTests[i]; executeTest(testPath); } }); ================================================ FILE: packages/grpc-web/scripts/build.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const fs = require("fs"); const path = require("path"); const {exec} = require("child_process"); const cwd = process.cwd(); const indexPath = path.relative(cwd, path.resolve(__dirname, "../index.js")); const jsPaths = [ "../exports.js", "../../../javascript", "../node_modules/google-closure-library", ].map(jsPath => path.relative(cwd, path.resolve(__dirname, jsPath))); const closureArgs = [].concat( jsPaths.map(jsPath => `--js=${jsPath}`), [ `--entry_point=grpc.web.Exports`, `--externs=externs.js`, `--dependency_mode=PRUNE`, `--compilation_level=ADVANCED_OPTIMIZATIONS`, `--generate_exports`, `--export_local_property_definitions`, `--js_output_file=${indexPath}`, ] ); const closureCompilerBin = path.resolve(__dirname, "../node_modules/.bin/google-closure-compiler"); const closureCommand = closureCompilerBin + " " + closureArgs.join(' '); console.log(closureCommand); let child = exec(closureCommand); child.stdout.pipe(process.stdout); child.stderr.pipe(process.stderr); function createSymlink(target, path) { fs.symlink(target, path, (err) => { if (err && err.code != 'EEXIST') { throw err; } }); } createSymlink(path.resolve(__dirname, "../index.js"), path.resolve(__dirname, "../node_modules/grpc-web.js")); createSymlink(path.resolve(__dirname, "../index.d.ts"), path.resolve(__dirname, "../node_modules/grpc-web.d.ts")); ================================================ FILE: packages/grpc-web/scripts/common.py ================================================ # Copyright 2021 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Common methods and constants for generating test files.""" import os from typing import Iterator # The directory in which test HTML files are generated. GENERATED_TEST_BASE_PATH = "generated/test_htmls/" def read_file(path: str) -> str: """Reads the content of a file.""" with open(path) as f: return f.read() def write_file(path: str, content: str): """Writes a string to file, overwriting existing content; intermediate directories are created if not present.""" dir_name = os.path.dirname(path) if not os.path.exists(dir_name): os.makedirs(dir_name) with open(path, "w") as f: f.write(content) def get_files_with_suffix(root_dir: str, suffix: str) -> Iterator[str]: """Yields file names under a directory with a given suffix.""" for dir_path, _, file_names in os.walk(root_dir): for file_name in file_names: if file_name.endswith(suffix): yield os.path.join(dir_path, file_name) ================================================ FILE: packages/grpc-web/scripts/gen_all_tests_js.py ================================================ # Copyright 2021 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Generates the all_tests.js file for consumption by Protractor. Usage: $ cd packages/grpc-web $ python3 ./scripts/gen_test_htmls.py # Prerequisite $ python3 ./scripts/gen_all_tests_js.py """ from string import Template import common ALL_TESTS_TEMPLATE_FILE = './scripts/template_all_tests_js.txt' # The path of the generated all_tests.js file GENERATED_ALL_TESTS_JS_PATH = './generated/all_tests.js' # File paths needs to be prepended by the relative path of the grpc-web package # because web server is hosting the root of github repo for tests to access the # javascript files. GRPC_WEB_BASE_DIR = 'packages/grpc-web' def main(): template_data = common.read_file(ALL_TESTS_TEMPLATE_FILE) template = Template(template_data) test_html_paths = [] for file_name in common.get_files_with_suffix( common.GENERATED_TEST_BASE_PATH, '_test.html'): test_html_paths.append(" '%s/%s'," % (GRPC_WEB_BASE_DIR, file_name)) # Example output paths: # 'packages/grpc-web/generated/test_htmls/javascript__net__grpc__web__grpcwebclientbase_test.html', # 'packages/grpc-web/generated/test_htmls/javascript__net__grpc__web__grpcwebstreamparser_test.html', test_html_paths_str = "\n".join(test_html_paths) # Writes the generated output to the all_tests.js file. common.write_file(GENERATED_ALL_TESTS_JS_PATH, template.substitute(test_html_paths=test_html_paths_str)) if __name__ == "__main__": main() ================================================ FILE: packages/grpc-web/scripts/gen_test_htmls.py ================================================ # Copyright 2021 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Generates *_test.html files from *_test.js files. Usage: $ cd packages/grpc-web $ python3 ./scripts/gen_test_htmls.py """ import os import re from string import Template import common # The directories containing JS tests. DIRECTORIES_WITH_TESTS = ["../../javascript"] TEST_HTML_TEMPLATE_FILE = './scripts/template_test_html.txt' def main(): template_data = common.read_file(TEST_HTML_TEMPLATE_FILE) template = Template(template_data) for directory in DIRECTORIES_WITH_TESTS: for js_file_path in common.get_files_with_suffix( directory, "_test.js"): _gen_test_html(js_file_path, template) def _gen_test_html(js_file_path: str, template: Template): """Generates a Closure test wrapper HTML and saves it to the filesystem.""" # Generates the test_file_name so that: # ../../javascript/net/grpc/web/grpcwebclientbase_test.js # will now be named: # javascript__net__grpc__web__grpcwebclientbase_test.html test_file_name = js_file_path while test_file_name.startswith('../'): test_file_name = test_file_name[3:] test_file_name = test_file_name.replace('/', '__') test_file_name = os.path.splitext(test_file_name)[0] + '.html' # Generates the test HTML using the package name of the test file package_name = _extract_closure_package(js_file_path) generated_html = template.substitute(package=package_name) # Writes the test HTML files common.write_file(common.GENERATED_TEST_BASE_PATH + test_file_name, generated_html) def _extract_closure_package(js_file_path) -> str: """Extracts the package name from goog.provide() or goog.module() in the JS file.""" js_data = common.read_file(js_file_path) matches = re.search(r"goog\.(provide|module)\([\n\s]*'(.+)'\);", js_data) if matches is None: raise ValueError("goog.provide() or goog.module() not found in file") return matches.group(2) if __name__ == "__main__": main() ================================================ FILE: packages/grpc-web/scripts/generate_test_files.sh ================================================ #!/bin/bash # Copyright 2021 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Generates the temporary files needed for tests to run, putting them in the # generated/ directory. # # Usage: # $ cd packages/grpc-web # $ ./scripts/generate_test_files.sh set -ex SCRIPT_DIR=$(dirname "$0") REPO_DIR=$(realpath "${SCRIPT_DIR}/../") JAVASCRIPT_DIR=$(realpath "${SCRIPT_DIR}/../../../javascript") GEN_DIR="$REPO_DIR/generated" cd "$REPO_DIR" mkdir -p "$GEN_DIR" echo "Generating dependency file..." npx closure-make-deps \ --closure-path="node_modules/google-closure-library/closure/goog" \ --file="node_modules/google-closure-library/closure/goog/deps.js" \ --root="$JAVASCRIPT_DIR" \ --exclude="$GEN_DIR/all_tests.js" \ --exclude="$GEN_DIR/deps.js" \ > "$GEN_DIR/deps.js" echo "Generating test HTML files..." python3 ./scripts/gen_test_htmls.py python3 ./scripts/gen_all_tests_js.py echo "Done." ================================================ FILE: packages/grpc-web/scripts/run_jsunit_tests.sh ================================================ #!/bin/bash # Copyright 2021 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This script starts a local HTTP server to serve test files, starts a Selenium Webdriver, and # runs the unit tests using Protractor. # Run locally with Pratractor: # # Usage (under ./packages/grpc-web): # $ ./scripts/generate_test_files.sh # Required first step # $ ./scripts/run_jsunit_tests.sh # # Or (preferred use): # $ npm run test-jsunit set -e cd "$(dirname $(dirname "$0"))" PROTRACTOR_BIN_PATH="./node_modules/protractor/bin" function cleanup () { echo "Killing HTTP Server..." kill $serverPid } # Start the local webserver. echo "Starting local HTP Server..." npx gulp serve & serverPid=$! echo "Local HTTP Server started with PID $serverPid." trap cleanup EXIT echo "Using Headless Chrome." # Updates Selenium Webdriver. echo "$PROTRACTOR_BIN_PATH/webdriver-manager update --versions.chrome=112.0.5615.165 --gecko=false" $PROTRACTOR_BIN_PATH/webdriver-manager update --versions.chrome=112.0.5615.165 --gecko=false # Run the tests using Protractor! (Protractor should run selenium automatically) $PROTRACTOR_BIN_PATH/protractor protractor.conf.js ================================================ FILE: packages/grpc-web/scripts/template_all_tests_js.txt ================================================ var allTests = [ $test_html_paths ]; if (typeof module !== 'undefined' && module.exports) { module.exports = allTests; } ================================================ FILE: packages/grpc-web/scripts/template_test_html.txt ================================================ ================================================ FILE: packages/grpc-web/test/closure_client.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ goog.provide('proto.grpc.gateway.testing.EchoAppClient'); goog.require('proto.grpc.gateway.testing.EchoServiceClient'); goog.require('proto.grpc.gateway.testing.EchoRequest'); proto.grpc.gateway.testing.EchoAppClient = function() { this.client = new proto.grpc.gateway.testing.EchoServiceClient('MyHostname', null, null); } proto.grpc.gateway.testing.EchoAppClient.prototype.echo = function(msg, cb) { var request = new proto.grpc.gateway.testing.EchoRequest(); request.setMessage(msg); this.client.echo(request, {}, cb); } ================================================ FILE: packages/grpc-web/test/common.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const fs = require('fs'); const GENERATED_CODE_PATH = './generated'; var removeDirectory = function(path) { if (fs.existsSync(path)) { fs.readdirSync(path).forEach(function(file, index) { var curPath = path + "/" + file; if (fs.lstatSync(curPath).isDirectory()) { removeDirectory(curPath); } else { fs.unlinkSync(curPath); } }); fs.rmdirSync(path); } } exports.removeDirectory = removeDirectory; exports.GENERATED_CODE_PATH = GENERATED_CODE_PATH; ================================================ FILE: packages/grpc-web/test/eval_test.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const assert = require('assert'); const execSync = require('child_process').execSync; const commandExists = require('command-exists').sync; const fs = require('fs'); const path = require('path'); const removeDirectory = require('./common.js').removeDirectory; const GENERATED_CODE_PATH = require('./common.js').GENERATED_CODE_PATH; describe('grpc-web generated code eval test (commonjs+dts)', function() { const genCodeCmd = 'protoc -I=./test/protos foo.proto models.proto ' + '--js_out=import_style=commonjs:./test/generated ' + '--grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:' + './test/generated'; before(function() { ['protoc', 'protoc-gen-grpc-web'].map(prog => { if (!commandExists(prog)) { assert.fail(`${prog} is not installed`); } }); }); beforeEach(function() { removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); fs.mkdirSync(path.resolve(__dirname, GENERATED_CODE_PATH)); }); afterEach(function() { removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); }); it('should eval', function() { execSync(genCodeCmd); execSync(`npx gulp --gulpfile ./test/gulpfile.js gen-code-eval-test`); }) }); describe('grpc-web generated code eval test (typescript)', function() { const genTsCodePath = path.resolve(__dirname, './generated/FooServiceClientPb.ts'); const genCodeCmd = 'protoc -I=./test/protos foo.proto models.proto ' + '--js_out=import_style=commonjs:./test/generated ' + '--grpc-web_out=import_style=typescript,mode=grpcwebtext:./test/generated'; before(function() { ['protoc', 'protoc-gen-grpc-web'].map(prog => { if (!commandExists(prog)) { assert.fail(`${prog} is not installed`); } }); }); beforeEach(function() { removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); fs.mkdirSync(path.resolve(__dirname, GENERATED_CODE_PATH)); }); afterEach(function() { removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); }); it('should eval', function() { execSync(genCodeCmd); // --skipLibCheck is needed because some of our node_modules/ targets es6 // but our test doesn't pass with `--target es6` // TODO: Find out how we can enable --target es6! execSync(`tsc --strict --skipLibCheck ${genTsCodePath}`); }); }); ================================================ FILE: packages/grpc-web/test/export_test.js ================================================ const assert = require('assert'); const grpc = {}; grpc.web = require('grpc-web'); describe('grpc-web export test', function() { it('should have MethodDescriptor exported', function() { assert.equal(typeof grpc.web.MethodDescriptor, 'function'); }); it('should have GrpcWebClientBase#rpcCall() exported', function() { assert.equal(typeof grpc.web.GrpcWebClientBase.prototype.rpcCall, 'function'); }); it('should have GrpcWebClientBase#serverStreaming() exported', function() { assert.equal(typeof grpc.web.GrpcWebClientBase.prototype.serverStreaming, 'function'); }); it('should have RpcError properties exported', function() { const rpcError = new grpc.web.RpcError(/* code= */ 0, 'message'); assert.equal(typeof rpcError.code, 'number'); assert.equal(typeof rpcError.message, 'string'); assert.equal(typeof rpcError.metadata, 'object'); }); it('should have StatusCode exported', function() { assert.deepEqual(grpc.web.StatusCode, { ABORTED: 10, ALREADY_EXISTS: 6, CANCELLED: 1, DATA_LOSS: 15, DEADLINE_EXCEEDED: 4, FAILED_PRECONDITION: 9, INTERNAL: 13, INVALID_ARGUMENT: 3, NOT_FOUND: 5, OK: 0, OUT_OF_RANGE: 11, PERMISSION_DENIED: 7, RESOURCE_EXHAUSTED: 8, UNAUTHENTICATED: 16, UNAVAILABLE: 14, UNIMPLEMENTED: 12, UNKNOWN: 2 }); }); }); ================================================ FILE: packages/grpc-web/test/generated_code_test.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const assert = require('assert'); const execSync = require('child_process').execSync; const commandExists = require('command-exists').sync; const fs = require('fs'); const path = require('path'); const removeDirectory = require('./common.js').removeDirectory; const GENERATED_CODE_PATH = require('./common.js').GENERATED_CODE_PATH; const mockXmlHttpRequest = require('mock-xmlhttprequest'); var MockXMLHttpRequest; function multiDone(done, count) { return function() { count -= 1; if (count <= 0) { done(); } }; } describe('protoc generated code', function() { const genCodePath = path.resolve(__dirname, './echo_pb.js'); const genCodeCmd = 'protoc -I=./test/protos echo.proto ' + '--js_out=import_style=commonjs:./test'; before(function() { if (!commandExists('protoc')) { assert.fail('protoc is not installed'); } }); beforeEach(function() { if (fs.existsSync(genCodePath)) { fs.unlinkSync(genCodePath); } }); afterEach(function() { if (fs.existsSync(genCodePath)) { fs.unlinkSync(genCodePath); } }); it('should exist', function() { execSync(genCodeCmd); assert.equal(true, fs.existsSync(genCodePath)); }); it('should import', function() { execSync(genCodeCmd); const {EchoRequest} = require(genCodePath); var req = new EchoRequest(); req.setMessage('abc'); assert.equal('abc', req.getMessage()); }); }); describe('grpc-web generated code: promise-based client', function() { const protoGenCodePath = path.resolve(__dirname, './echo_pb.js'); const genCodePath = path.resolve(__dirname, './echo_grpc_web_pb.js'); const genCodeCmd = 'protoc -I=./test/protos echo.proto ' + '--js_out=import_style=commonjs:./test ' + '--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./test'; before(function() { MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr() global.XMLHttpRequest = MockXMLHttpRequest; execSync(genCodeCmd); assert.equal(true, fs.existsSync(protoGenCodePath)); assert.equal(true, fs.existsSync(genCodePath)); }); after(function() { fs.unlinkSync(protoGenCodePath); fs.unlinkSync(genCodePath); }); it('should receive unary response', function(done) { const {EchoServicePromiseClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); var echoService = new EchoServicePromiseClient('MyHostname', null, null); var request = new EchoRequest(); request.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { // a single data frame with message 'aaa' assert.equal("AAAAAAUKA2FhYQ==", xhr.body); xhr.respond( 200, {'Content-Type': 'application/grpc-web-text'}, // a single data frame with message 'aaa' 'AAAAAAUKA2FhYQ=='); }; echoService.echo(request, {}) .then((response) => { assert.equal('aaa', response.getMessage()); done(); }); }); it('should receive error', function(done) { const {EchoServicePromiseClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); var echoService = new EchoServicePromiseClient('MyHostname', null, null); var request = new EchoRequest(); request.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 400, {'Content-Type': 'application/grpc-web-text'}); }; echoService.echo(request, {}) .then((response) => { assert.fail('should not receive response'); }) .catch((error) => { assert.equal(3, error.code); done(); }); }); }); describe('grpc-web generated code (commonjs+grpcwebtext)', function() { const oldXMLHttpRequest = global.XMLHttpRequest; const protoGenCodePath = path.resolve(__dirname, './echo_pb.js'); const genCodePath = path.resolve(__dirname, './echo_grpc_web_pb.js'); const genCodeCmd = 'protoc -I=./test/protos echo.proto ' + '--js_out=import_style=commonjs:./test ' + '--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./test'; before(function() { ['protoc', 'protoc-gen-grpc-web'].map(prog => { if (!commandExists(prog)) { assert.fail(`${prog} is not installed`); } }); }); beforeEach(function() { if (fs.existsSync(protoGenCodePath)) { fs.unlinkSync(protoGenCodePath); } if (fs.existsSync(genCodePath)) { fs.unlinkSync(genCodePath); } MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr() global.XMLHttpRequest = MockXMLHttpRequest; }); afterEach(function() { if (fs.existsSync(protoGenCodePath)) { fs.unlinkSync(protoGenCodePath); } if (fs.existsSync(genCodePath)) { fs.unlinkSync(genCodePath); } global.XMLHttpRequest = oldXMLHttpRequest; }); it('should exist', function() { execSync(genCodeCmd); assert.equal(true, fs.existsSync(genCodePath)); }); it('should import', function() { execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); var echoService = new EchoServiceClient('Bla', null, null); assert.equal('function', typeof echoService.echo); }); it('should send unary request', function(done) { execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); var echoService = new EchoServiceClient('MyHostname', null, null); var request = new EchoRequest(); request.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { assert.equal('POST', xhr.method); // a single 'aaa' string, encoded assert.equal('AAAAAAUKA2FhYQ==', xhr.body); assert.equal('MyHostname/grpc.gateway.testing.EchoService/Echo', xhr.url); assert.equal( 'accept: application/grpc-web-text\r\n' + 'content-type: application/grpc-web-text\r\n' + 'custom-header-1: value1\r\n' + 'x-grpc-web: 1\r\n' + 'x-user-agent: grpc-web-javascript/0.1\r\n', xhr.requestHeaders.getAll()); done(); }; echoService.echo(request, {'custom-header-1':'value1'}); }); it('should receive unary response', function(done) { execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); var echoService = new EchoServiceClient('MyHostname', null, null); var request = new EchoRequest(); request.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, // a single data frame with 'aaa' message, encoded 'AAAAAAUKA2FhYQ=='); }; var call = echoService.echo(request, {'custom-header-1':'value1'}, function(err, response) { assert.equal('aaa', response.getMessage()); done(); }); call.on('data', (response) => { assert.fail('should not receive response this way'); }); }); it('should receive streaming response', function(done) { done = multiDone(done, 4); // done() should be called 4 times execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {ServerStreamingEchoRequest} = require(protoGenCodePath); var echoService = new EchoServiceClient('MyHostname', null, null); var request = new ServerStreamingEchoRequest(); request.setMessage('aaa'); request.setMessageCount(3); MockXMLHttpRequest.onSend = function(xhr) { // a proto message of 1: "aaa", 2: 3, base64-encoded assert.equal("AAAAAAcKA2FhYRAD", xhr.body); xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, // 3 'aaa' messages in 3 data frames, encoded 'AAAAAAUKA2FhYQAAAAAFCgNhYWEAAAAABQoDYWFh'); }; var stream = echoService.serverStreamingEcho(request, {}); stream.on('data', function(response) { assert.equal('aaa', response.getMessage()); done(); }); stream.on('end', function() { done(); }); }); it('should receive trailing metadata', function(done) { done = multiDone(done, 2); execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); var echoService = new EchoServiceClient('MyHostname', null, null); var request = new EchoRequest(); request.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 200, {'Content-Type': 'application/grpc-web-text'}, // a single data frame with an 'aaa' message, followed by, // a trailer frame with content 'grpc-status: 0\d\ax-custom-1: ababab' 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDANCngtY3VzdG9tLTE6IGFiYWJhYg0K' ); }; var call = echoService.echo( request, {'custom-header-1':'value1'}, function(err, response) { if (err) { assert.fail('should not receive error'); } assert(response); assert.equal('aaa', response.getMessage()); done(); }); call.on('status', function(status) { assert.equal(0, status.code); assert.equal('object', typeof status.metadata); assert.equal(false, 'grpc-status' in status.metadata); assert.equal(true, 'x-custom-1' in status.metadata); assert.equal('ababab', status.metadata['x-custom-1']); done(); }); }); it('should receive error', function(done) { execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); var echoService = new EchoServiceClient('MyHostname', null, null); var request = new EchoRequest(); request.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, // a trailer frame with content 'grpc-status:10' 'gAAAABBncnBjLXN0YXR1czoxMA0K'); }; var call = echoService.echo( request, {'custom-header-1':'value1'}, function(err, response) { if (response) { assert.fail('should not have received response'); } assert(err); assert.equal(10, err.code); done(); }); call.on('error', (error) => { assert.fail('error callback should not be called for unary calls'); }); }); it('should error out on incomplete response', function(done) { execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); var echoService = new EchoServiceClient('MyHostname', null, null); var request = new EchoRequest(); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, // An incomplete response. The frame length indicates // 26 bytes, but the rest of the frame only contains // 18 bytes. 'AAAAABoKCwgBEgdGaWN0aW9uCgsIAhI'); }; var call = echoService.echo( request, {}, function(err, response) { if (response) { assert.fail('should not receive response'); } assert.equal(2, err.code); assert.equal(true, err.message.toLowerCase().includes( 'incomplete response')); done(); }); call.on('data', (response) => { assert.fail('should not receive response this way'); }); call.on('error', (error) => { assert.fail('should not receive error this way'); }); }); it('should error out on invalid proto response', function(done) { execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); var echoService = new EchoServiceClient('MyHostname', null, null); var request = new EchoRequest(); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, // A valid grpc-web frame, but contains an invalid // protobuf payload. 'AAAAAAUKCgoKCg=='); }; var call = echoService.echo( request, {}, function(err, response) { if (response) { assert.fail('should not receive response'); } assert.equal(13 /* StatusCode.INTERNAL */, err.code); assert.equal(true, err.message.toLowerCase().includes('deserializing')); assert.equal(true, err.message.toLowerCase().includes('error')); done(); }); call.on('data', (response) => { assert.fail('should not receive response this way'); }); call.on('error', (error) => { assert.fail('should not receive error this way'); }); }); it('should error out on invalid response body', function(done) { execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); var echoService = new EchoServiceClient('MyHostname', null, null); var request = new EchoRequest(); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, // An invalid response body. Should trip up in the // stream parser. 'ZZZZZ'); }; var call = echoService.echo( request, {}, function(err, response) { if (response) { assert.fail('should not receive response'); } assert.equal(2, err.code); assert.equal(true, err.message.toLowerCase().includes( 'error in parsing response body')); done(); }); call.on('data', (response) => { assert.fail('should not receive response this way'); }); call.on('error', (error) => { assert.fail('should not receive error this way'); }); }); it('should not receive response on non-ok status', function(done) { done = multiDone(done, 2); execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); var echoService = new EchoServiceClient('MyHostname', null, null); var request = new EchoRequest(); request.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 200, {'Content-Type': 'application/grpc-web-text'}, // a single data frame with an 'aaa' message, followed by, // a trailer frame with content 'grpc-status: 2\d\ax-custom-1: ababab' 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDINCngtY3VzdG9tLTE6IGFiYWJhYg0K' ); }; var call = echoService.echo( request, {'custom-header-1':'value1'}, function(err, response) { if (response) { assert.fail('should not have received response with non-OK status'); } else { assert.equal(2, err.code); } done(); }); call.on('status', function(status) { assert.equal(2, status.code); assert.equal('object', typeof status.metadata); assert.equal(false, 'grpc-status' in status.metadata); assert.equal(true, 'x-custom-1' in status.metadata); assert.equal('ababab', status.metadata['x-custom-1']); done(); }); call.on('error', (error) => { assert.fail('error callback should not be called for unary calls'); }); }); }); describe('grpc-web generated code (closure+grpcwebtext)', function() { const oldXMLHttpRequest = global.XMLHttpRequest; const compiledCodePath = path.resolve(__dirname, './generated/compiled.js'); const genCodeCmd = 'protoc -I=./test/protos echo.proto ' + '--js_out=import_style=closure:./test/generated ' + '--grpc-web_out=import_style=closure,mode=grpcwebtext:./test/generated'; const cwd = process.cwd(); const jsPaths = [ ".", "../../../javascript", "../node_modules/google-closure-library", "../../../third_party/protobuf/js", ].map(jsPath => path.relative(cwd, path.resolve(__dirname, jsPath))); const closureArgs = [].concat( jsPaths.map(jsPath => `--js=${jsPath}`), [ `--entry_point=goog:proto.grpc.gateway.testing.EchoAppClient`, `--dependency_mode=PRUNE`, `--js_output_file ./test/generated/compiled.js`, `--output_wrapper="%output%module.exports = `+ `proto.grpc.gateway.testing;"`, ] ); const closureCmd = "google-closure-compiler " + closureArgs.join(' '); before(function() { ['protoc', 'protoc-gen-grpc-web'].map(prog => { if (!commandExists(prog)) { assert.fail(`${prog} is not installed`); } }); if (!fs.existsSync(path.resolve( __dirname, '../../../javascript/net/grpc/web/grpcwebclientbase.js'))) { this.skip(); } removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); fs.mkdirSync(path.resolve(__dirname, GENERATED_CODE_PATH)); }); beforeEach(function() { MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr() global.XMLHttpRequest = MockXMLHttpRequest; }); afterEach(function() { global.XMLHttpRequest = oldXMLHttpRequest; }); after(function() { removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); }); it('should exist', function() { execSync(genCodeCmd); execSync(closureCmd); assert.equal(true, fs.existsSync(compiledCodePath)); }); it('should import', function() { var compiled = require(compiledCodePath); echoAppClient = new compiled.EchoAppClient(); assert.equal('function', typeof echoAppClient.echo); }); it('should send unary request', function(done) { var compiled = require(compiledCodePath); echoAppClient = new compiled.EchoAppClient(); MockXMLHttpRequest.onSend = function(xhr) { assert.equal("AAAAAAUKA2FiYw==", xhr.body); done(); } echoAppClient.echo('abc', () => {}); }); it('should receive unary response', function(done) { var compiled = require(compiledCodePath); echoAppClient = new compiled.EchoAppClient(); MockXMLHttpRequest.onSend = function(xhr) { assert.equal("AAAAAAUKA2FiYw==", xhr.body); xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, "AAAAAAUKA2FiYw=="); } echoAppClient.echo('abc', function(err, response) { assert.equal("abc", response.getMessage()); done(); }); }); }); describe('grpc-web generated code: callbacks tests', function() { const protoGenCodePath = path.resolve(__dirname, './echo_pb.js'); const genCodePath = path.resolve(__dirname, './echo_grpc_web_pb.js'); const genCodeCmd = 'protoc -I=./test/protos echo.proto ' + '--js_out=import_style=commonjs:./test ' + '--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./test'; var echoService; var request; before(function() { MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr() global.XMLHttpRequest = MockXMLHttpRequest; execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); echoService = new EchoServiceClient('MyHostname', null, null); request = new EchoRequest(); request.setMessage('aaa'); }); after(function() { if (fs.existsSync(protoGenCodePath)) { fs.unlinkSync(protoGenCodePath); } if (fs.existsSync(genCodePath)) { fs.unlinkSync(genCodePath); } }); it('should receive initial metadata callback', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 200, { 'Content-Type': 'application/grpc-web-text', 'initial-header-1': 'value1', }, // a single data frame with message 'aaa' 'AAAAAAUKA2FhYQ=='); }; var call = echoService.echo( request, {}, function(err, response) { if (err) { assert.fail('should not have received error'); } else { assert.equal('aaa', response.getMessage()); } done(); } ); call.on('metadata', (metadata) => { assert('initial-header-1' in metadata); assert.equal('value1', metadata['initial-header-1']); done(); }); }); it('should receive error, on http error', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 400, {'Content-Type': 'application/grpc-web-text'}); }; var call = echoService.echo( request, {}, function(err, response) { if (response) { assert.fail('should not have received response with non-OK status'); } else { assert.equal(3, err.code); // http error 400 mapped to grpc error 3 } done(); } ); call.on('status', (status) => { assert.equal(3, status.code); done(); }); call.on('error', (error) => { assert.fail('error callback should not be called for unary calls'); }); }); it('should receive error, on grpc error', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 200, {'Content-Type': 'application/grpc-web-text'}, // a single data frame with an 'aaa' message, followed by, // a trailer frame with content 'grpc-status: 2\d\ax-custom-1: ababab' 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDINCngtY3VzdG9tLTE6IGFiYWJhYg0K' ); }; var call = echoService.echo( request, {}, function(err, response) { if (response) { assert.fail('should not have received response with non-OK status'); } else { assert.equal(2, err.code); assert.equal(true, 'x-custom-1' in err.metadata); assert.equal('ababab', err.metadata['x-custom-1']); } done(); } ); // also should receive trailing status callback call.on('status', (status) => { // grpc-status should not be part of trailing metadata assert.equal(false, 'grpc-status' in status.metadata); assert.equal(true, 'x-custom-1' in status.metadata); assert.equal('ababab', status.metadata['x-custom-1']); done(); }); call.on('error', (error) => { assert.fail('error callback should not be called for unary calls'); }); }); it('should receive error, on response header error', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 200, { 'Content-Type': 'application/grpc-web-text', 'grpc-status': 2, 'grpc-message': 'some error', }); }; var call = echoService.echo( request, {}, function(err, response) { if (response) { assert.fail('should not have received response with non-OK status'); } else { assert.equal(2, err.code); assert.equal('some error', err.message); } done(); } ); call.on('status', (status) => { assert.equal(2, status.code); done(); }); call.on('error', (error) => { assert.fail('error callback should not be called for unary calls'); }); }); it('should receive status callback', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 200, {'Content-Type': 'application/grpc-web-text'}, // a single data frame with an 'aaa' message, followed by, // a trailer frame with content 'grpc-status: 0\d\ax-custom-1: ababab' 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDANCngtY3VzdG9tLTE6IGFiYWJhYg0K' ); }; var call = echoService.echo( request, {}, function(err, response) { if (err) { assert.fail('should not receive error'); } assert(response); assert.equal('aaa', response.getMessage()); done(); } ); call.on('status', (status) => { assert.equal(0, status.code); // grpc-status should not be part of trailing metadata assert.equal(false, 'grpc-status' in status.metadata); assert.equal(true, 'x-custom-1' in status.metadata); assert.equal('ababab', status.metadata['x-custom-1']); done(); }); call.on('error', (error) => { assert.fail('error callback should not be called for unary calls'); }); }); it('should trigger multiple callbacks on same event', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 400, {'Content-Type': 'application/grpc-web-text'}); }; var call = echoService.serverStreamingEcho(request, {}); call.on('data', (response) => { assert.fail('should not have received a data callback'); }); call.on('error', (error) => { assert.equal(3, error.code); // http error 400 mapped to grpc error 3 done(); }); call.on('error', (error) => { assert.equal(3, error.code); // http error 400 mapped to grpc error 3 done(); }); }); it('should be able to remove callback', function(done) { MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 400, {'Content-Type': 'application/grpc-web-text'}); }; var call = echoService.serverStreamingEcho(request, {}); const callbackA = (error) => { assert.equal(3, error.code); // http error 400 mapped to grpc error 3 done(); } const callbackB = (error) => { assert.fail('should not be called'); } call.on('error', callbackA); call.on('error', callbackB); call.removeListener('error', callbackB); }); it('should receive initial metadata callback (streaming)', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 200, { 'Content-Type': 'application/grpc-web-text', 'initial-header-1': 'value1', }, // a single data frame with message 'aaa' 'AAAAAAUKA2FhYQ=='); }; var call = echoService.serverStreamingEcho(request, {}); call.on('data', (response) => { assert.equal('aaa', response.getMessage()); done(); }); call.on('metadata', (metadata) => { assert('initial-header-1' in metadata); assert.equal('value1', metadata['initial-header-1']); done(); }); }); it('should receive error, on http error (streaming)', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 400, {'Content-Type': 'application/grpc-web-text'}); }; var call = echoService.serverStreamingEcho(request, {}); call.on('data', (response) => { assert.fail('should not receive data response'); }); call.on('status', (status) => { assert.equal(3, status.code); done(); }); call.on('error', (error) => { assert.equal(3, error.code); done(); }); }); it('should receive error, on grpc error (streaming)', function(done) { done = multiDone(done, 3); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 200, {'Content-Type': 'application/grpc-web-text'}, // a single data frame with an 'aaa' message, followed by, // a trailer frame with content 'grpc-status: 2\d\ax-custom-1: ababab' 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDINCngtY3VzdG9tLTE6IGFiYWJhYg0K' ); }; var call = echoService.serverStreamingEcho(request, {}); call.on('data', (response) => { // because this is a streaming call, we should still receive data // callbacks if the error comes in with the trailer frame assert.equal('aaa', response.getMessage()); done(); }); call.on('error', (error) => { assert.equal(2, error.code); assert.equal(true, 'x-custom-1' in error.metadata); assert.equal('ababab', error.metadata['x-custom-1']); done(); }); call.on('status', (status) => { assert.equal(2, status.code); // grpc-status should not be part of trailing metadata assert.equal(false, 'grpc-status' in status.metadata); assert.equal(true, 'x-custom-1' in status.metadata); assert.equal('ababab', status.metadata['x-custom-1']); done(); }); }); it('should receive error, on response header error (streaming)', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 200, { 'Content-Type': 'application/grpc-web-text', 'grpc-status': 2, 'grpc-message': 'some error', }); }; var call = echoService.serverStreamingEcho(request, {}); call.on('error', (error) => { assert.equal(2, error.code); assert.equal('some error', error.message); done(); }); call.on('status', (status) => { assert.equal(2, status.code); assert.equal('some error', status.details); done(); }); }); it('should receive status callback (streaming)', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond( 200, {'Content-Type': 'application/grpc-web-text'}, // a single data frame with an 'aaa' message, followed by, // a trailer frame with content 'grpc-status: 0\d\ax-custom-1: ababab' 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDANCngtY3VzdG9tLTE6IGFiYWJhYg0K' ); }; var call = echoService.serverStreamingEcho(request, {}); call.on('data', (response) => { assert(response); assert.equal('aaa', response.getMessage()); done(); }); call.on('status', (status) => { assert.equal(0, status.code); // grpc-status should not be part of trailing metadata assert.equal(false, 'grpc-status' in status.metadata); assert.equal(true, 'x-custom-1' in status.metadata); assert.equal('ababab', status.metadata['x-custom-1']); done(); }); call.on('error', (error) => { assert.fail('error callback should not be called for unary calls'); }); }); }); ================================================ FILE: packages/grpc-web/test/gulpfile.js ================================================ const gulp = require('gulp'); const gulpEval = require('gulp-eval'); gulp.task('gen-code-eval-test', () => gulp.src('./generated/foo_grpc_web_pb.js') .pipe(gulpEval()) ); ================================================ FILE: packages/grpc-web/test/plugin_test.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const assert = require('assert'); const execSync = require('child_process').execSync; const commandExists = require('command-exists').sync; const fs = require('fs'); const path = require('path'); const removeDirectory = require('./common.js').removeDirectory; const GENERATED_CODE_PATH = require('./common.js').GENERATED_CODE_PATH; const mockXmlHttpRequest = require('mock-xmlhttprequest'); var MockXMLHttpRequest; describe('grpc-web plugin test, with subdirectories', function() { const oldXMLHttpRequest = global.XMLHttpRequest; const genCodePath1 = path.resolve( __dirname, GENERATED_CODE_PATH + '/myapi/v1/myapi_pb.js'); const genCodePath2 = path.resolve( __dirname, GENERATED_CODE_PATH + '/otherapi/v1/otherapi_pb.js'); const genCodePath3 = path.resolve( __dirname, GENERATED_CODE_PATH + '/myapi/v1/myapi_grpc_web_pb.js'); const genCodeCmd = 'protoc -I=./test/protos ' + './test/protos/myapi/v1/myapi.proto ' + './test/protos/otherapi/v1/otherapi.proto ' + '--js_out=import_style=commonjs:./test/generated ' + '--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./test/generated'; before(function() { ['protoc', 'protoc-gen-grpc-web'].map(prog => { if (!commandExists(prog)) { assert.fail(`${prog} is not installed`); } }); }); beforeEach(function() { removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); fs.mkdirSync(path.resolve(__dirname, GENERATED_CODE_PATH)); MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr(); global.XMLHttpRequest = MockXMLHttpRequest; }); afterEach(function() { removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); global.XMLHttpRequest = oldXMLHttpRequest; }); it('should exist', function() { execSync(genCodeCmd); assert.equal(true, fs.existsSync(genCodePath1)); assert.equal(true, fs.existsSync(genCodePath2)); assert.equal(true, fs.existsSync(genCodePath3)); }); it('should import', function() { execSync(genCodeCmd); const {OtherThing} = require(genCodePath2); var otherThing = new OtherThing(); otherThing.setValue('abc'); assert.equal('abc', otherThing.getValue()); const {MyServiceClient} = require(genCodePath3); var myClient = new MyServiceClient("MyHostname", null, null); assert.equal('function', typeof myClient.doThis); }); it('should send unary request', function(done) { execSync(genCodeCmd); const {OtherThing} = require(genCodePath2); var otherThing = new OtherThing(); otherThing.setValue('abc'); const {MyServiceClient} = require(genCodePath3); var myClient = new MyServiceClient("MyHostname", null, null); MockXMLHttpRequest.onSend = function(xhr) { assert.equal("AAAAAAUKA2FiYw==", xhr.body); assert.equal("MyHostname/myproject.myapi.v1.MyService/DoThis", xhr.url); done(); }; myClient.doThis(otherThing); }); }); describe('grpc-web plugin test, with multiple input files', function() { const genCodePath1 = path.resolve( __dirname, GENERATED_CODE_PATH + '/myapi/v1/myapi_pb.js'); const genCodePath2 = path.resolve( __dirname, GENERATED_CODE_PATH + '/otherapi/v1/otherapi_pb.js'); const genCodePath3 = path.resolve( __dirname, GENERATED_CODE_PATH + '/myapi/v1/myapi_grpc_web_pb.js'); const genCodePath4 = path.resolve( __dirname, GENERATED_CODE_PATH + '/myapi/v1/myapi-two_grpc_web_pb.js'); const genCodeCmd = 'protoc -I=./test/protos ' + './test/protos/myapi/v1/myapi.proto ' + './test/protos/myapi/v1/myapi-two.proto ' + './test/protos/otherapi/v1/otherapi.proto ' + '--js_out=import_style=commonjs:./test/generated ' + '--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./test/generated'; before(function() { ['protoc', 'protoc-gen-grpc-web'].map(prog => { if (!commandExists(prog)) { assert.fail(`${prog} is not installed`); } }); }); beforeEach(function() { removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); fs.mkdirSync(path.resolve(__dirname, GENERATED_CODE_PATH)); }); afterEach(function() { removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); }); it('should exist', function() { execSync(genCodeCmd); assert.equal(true, fs.existsSync(genCodePath1)); assert.equal(true, fs.existsSync(genCodePath2)); assert.equal(true, fs.existsSync(genCodePath3)); assert.equal(true, fs.existsSync(genCodePath4)); }); it('should import', function() { execSync(genCodeCmd); const {OtherThing} = require(genCodePath2); var otherThing = new OtherThing(); otherThing.setValue('abc'); assert.equal('abc', otherThing.getValue()); const {MyServiceClient} = require(genCodePath3); var myClient = new MyServiceClient("MyHostname", null, null); assert.equal('function', typeof myClient.doThis); const {MyServiceBClient} = require(genCodePath4); var myClientB = new MyServiceBClient("MyHostname", null, null); assert.equal('function', typeof myClientB.doThat); }); }); describe('grpc-web plugin test, proto with no package', function() { const genCodePath1 = path.resolve( __dirname, GENERATED_CODE_PATH + '/nopackage_pb.js'); const genCodePath2 = path.resolve( __dirname, GENERATED_CODE_PATH + '/nopackage_grpc_web_pb.js'); const genCodeCmd = 'protoc -I=./test/protos ' + './test/protos/nopackage.proto ' + '--js_out=import_style=commonjs:./test/generated ' + '--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./test/generated'; before(function() { ['protoc', 'protoc-gen-grpc-web'].map(prog => { if (!commandExists(prog)) { assert.fail(`${prog} is not installed`); } }); removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); fs.mkdirSync(path.resolve(__dirname, GENERATED_CODE_PATH)); MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr(); global.XMLHttpRequest = MockXMLHttpRequest; execSync(genCodeCmd); assert.equal(true, fs.existsSync(genCodePath1)); assert.equal(true, fs.existsSync(genCodePath2)); }); after(function() { removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); }); it('should import', function() { const {HelloRequest} = require(genCodePath1); var request = new HelloRequest(); request.setName('abc'); assert.equal('abc', request.getName()); }); it('callback-based generated client: should exist', function() { const {GreeterClient} = require(genCodePath2); var myClient = new GreeterClient("MyHostname", null, null); assert.equal('function', typeof myClient.sayHello); }); it('promise-based generated client: should exist', function() { const {HelloRequest} = require(genCodePath1); const {GreeterPromiseClient} = require(genCodePath2); var myClient = new GreeterPromiseClient("MyHostname", null, null); assert.equal('function', typeof myClient.sayHello); var p = myClient.sayHello(new HelloRequest(), {}); assert.equal('function', typeof p.then); }); }); ================================================ FILE: packages/grpc-web/test/protos/echo.proto ================================================ // Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package grpc.gateway.testing; message EchoRequest { string message = 1; int32 value = 2; } message EchoResponse { string message = 1; // to test Request and Response having a different shape string value = 2; } message ServerStreamingEchoRequest { string message = 1; int32 message_count = 2; int32 message_interval = 3; } message ServerStreamingEchoResponse { string message = 1; } enum Status { UNKNOWN = 0; SUCCESS = 1; } message EchoStatusRequest { Status status = 1; } message EchoStatusResponse { enum InternalStatus { UNKNOWN = 0; SUCCESS = 1; } InternalStatus status = 1; } service EchoService { rpc Echo(EchoRequest) returns (EchoResponse); rpc ServerStreamingEcho(ServerStreamingEchoRequest) returns (stream ServerStreamingEchoResponse); rpc EchoStatus(EchoStatusRequest) returns (EchoStatusResponse); } ================================================ FILE: packages/grpc-web/test/protos/foo.proto ================================================ syntax = "proto3"; package Foo; import "models.proto"; message SimpleRequest { string message = 1; } service FooService { rpc EchoEmpty(SimpleRequest) returns (models.SimpleMessage); } ================================================ FILE: packages/grpc-web/test/protos/models.proto ================================================ syntax = "proto3"; package models; message SimpleMessage { } ================================================ FILE: packages/grpc-web/test/protos/myapi/v1/myapi-two.proto ================================================ // Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package myproject.myapi.v1; import "otherapi/v1/otherapi.proto"; service MyServiceB { rpc DoThat(myproject.otherapi.v1.OtherThing) returns (myproject.otherapi.v1.OtherThing); } ================================================ FILE: packages/grpc-web/test/protos/myapi/v1/myapi.proto ================================================ // Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package myproject.myapi.v1; import "otherapi/v1/otherapi.proto"; message MyThing { string message = 1; myproject.otherapi.v1.OtherThing other_thing = 2; } service MyService { rpc DoThis(myproject.otherapi.v1.OtherThing) returns (myproject.otherapi.v1.OtherThing); } ================================================ FILE: packages/grpc-web/test/protos/nopackage.proto ================================================ // Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } ================================================ FILE: packages/grpc-web/test/protos/otherapi/v1/otherapi.proto ================================================ // Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package myproject.otherapi.v1; message OtherThing { string value = 1; } ================================================ FILE: packages/grpc-web/test/protos/test01.proto ================================================ syntax = "proto3"; message MessageOuter { message MessageInner { int32 value = 1; } enum EnumInner { DEFAULT = 0; } repeated MessageInner someProp = 1; EnumOuter someEnum = 2; EnumInner anotherEnum = 3; } enum EnumOuter { DEFAULT = 0; } ================================================ FILE: packages/grpc-web/test/protos/test02.proto ================================================ syntax = "proto3"; import "test03.proto"; service MyService { rpc addOne(Integer) returns (Integer); } ================================================ FILE: packages/grpc-web/test/protos/test03.proto ================================================ syntax = "proto3"; message Integer { } ================================================ FILE: packages/grpc-web/test/tsc-tests/client01.ts ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import * as grpcWeb from 'grpc-web'; import {MessageOuter} from './generated/test01_pb'; let inner1 = new MessageOuter.MessageInner(); inner1.setValue(123); let msgOuter = new MessageOuter(); msgOuter.setSomepropList([inner1]); export {msgOuter} ================================================ FILE: packages/grpc-web/test/tsc-tests/client02.ts ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import * as grpcWeb from 'grpc-web'; import {Integer} from './generated/test03_pb'; import {MyServiceClient} from './generated/Test02ServiceClientPb'; const service = new MyServiceClient('http://mydummy.com', null, null); const req = new Integer(); service.addOne(req, {}, (err: grpcWeb.RpcError, resp: Integer) => { }); ================================================ FILE: packages/grpc-web/test/tsc-tests/client03.ts ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import * as grpcWeb from 'grpc-web'; import {EchoRequest, EchoResponse} from './generated/echo_pb'; import {EchoServiceClient} from './generated/echo_grpc_web_pb'; // The StreamInterceptor interface is for the callback-based client. class MyStreamInterceptor implements grpcWeb.StreamInterceptor< EchoRequest, EchoResponse> { intercept( request: grpcWeb.Request, invoker: (request: grpcWeb.Request) => grpcWeb.ClientReadableStream) { class InterceptedStream implements grpcWeb.ClientReadableStream< EchoResponse> { stream: grpcWeb.ClientReadableStream; constructor(stream: grpcWeb.ClientReadableStream) { this.stream = stream; }; on(eventType: string, callback: any) { if (eventType == 'data') { const newCallback = (response: EchoResponse) => { response.setMessage('[-in-]'+response.getMessage()); callback(response); }; this.stream.on(eventType, newCallback); } else if (eventType == 'error') { this.stream.on('error', callback); } else if (eventType == 'metadata') { this.stream.on('metadata', callback); } else if (eventType == 'status') { this.stream.on('status', callback); } else if (eventType == 'end') { this.stream.on('end', callback); } return this; }; removeListener(eventType: string, callback: any) { } cancel() {} } var reqMsg = request.getRequestMessage(); reqMsg.setMessage('[-out-]'+reqMsg.getMessage()); return new InterceptedStream(invoker(request)); }; } var opts = {'streamInterceptors' : [new MyStreamInterceptor()]}; const echoService = new EchoServiceClient('http://localhost:8080', null, opts); export {echoService, EchoRequest} ================================================ FILE: packages/grpc-web/test/tsc-tests/client04.ts ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import * as grpcWeb from 'grpc-web'; import {EchoRequest, EchoResponse} from './generated/echo_pb'; import {EchoServicePromiseClient} from './generated/echo_grpc_web_pb'; // The UnaryInterceptor interface is for the promise-based client. class MyUnaryInterceptor implements grpcWeb.UnaryInterceptor< EchoRequest, EchoResponse> { intercept(request: grpcWeb.Request, invoker: (request: grpcWeb.Request) => Promise>) { const reqMsg = request.getRequestMessage(); reqMsg.setMessage('[-out-]' + reqMsg.getMessage()); return invoker(request).then((response: grpcWeb.UnaryResponse< EchoRequest, EchoResponse>) => { let result = '<-InitialMetadata->'; let initialMetadata = response.getMetadata(); for (let i in initialMetadata) { result += i + ': ' + initialMetadata[i]; } result += '<-TrailingMetadata->'; let trailingMetadata = response.getStatus().metadata; for (let i in trailingMetadata) { result += i + ': ' + trailingMetadata[i]; } const responseMsg = response.getResponseMessage(); result += '[-in-]' + responseMsg.getMessage(); responseMsg.setMessage(result); return response; }); } } var opts = {'unaryInterceptors' : [new MyUnaryInterceptor()]}; const echoService = new EchoServicePromiseClient('http://localhost:8080', null, opts); export {echoService, EchoRequest} ================================================ FILE: packages/grpc-web/test/tsc-tests/client05.ts ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import * as grpcWeb from 'grpc-web'; import {EchoRequest, EchoResponse, ServerStreamingEchoRequest, ServerStreamingEchoResponse} from './generated/echo_pb'; import {EchoServiceClient} from './generated/echo_grpc_web_pb'; const echoService = new EchoServiceClient('http://localhost:8080', null, null); let req = new EchoRequest(); req.setMessage('aaa'); // this test tries to make sure that these types are as accurate as possible let call1 : grpcWeb.ClientReadableStream = echoService.echo(req, {}, (err: grpcWeb.RpcError, response: EchoResponse) => { }); call1 .on('status', (status: grpcWeb.Status) => { }) .on('metadata', (metadata: grpcWeb.Metadata) => { }); let call2 : grpcWeb.ClientReadableStream = echoService.serverStreamingEcho(new ServerStreamingEchoRequest(), {}); call2 .on('data', (response: ServerStreamingEchoResponse) => { }) .on('error', (error: grpcWeb.RpcError) => { }); ================================================ FILE: packages/grpc-web/test/tsc-tests/client06.ts ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import * as grpcWeb from 'grpc-web'; import {EchoRequest, EchoResponse} from './generated/echo_pb'; import {EchoServicePromiseClient} from './generated/echo_grpc_web_pb'; const echoService = new EchoServicePromiseClient( 'http://localhost:8080', null, null); let req = new EchoRequest(); req.setMessage('aaa'); // this test tries to make sure that these types are as accurate as possible let p1 : Promise = echoService.echo(req, {}); // why does the .then() add this extra 'void' type to the returned Promise? let p2 : Promise = p1.then((response: EchoResponse) => { }); ================================================ FILE: packages/grpc-web/test/tsc_test.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const assert = require('assert'); const execSync = require('child_process').execSync; const fs = require('fs'); const path = require('path'); const removeDirectory = require('./common.js').removeDirectory; const mockXmlHttpRequest = require('mock-xmlhttprequest'); var MockXMLHttpRequest; function relativePath(relPath) { return path.resolve(__dirname, relPath); } function cleanup() { removeDirectory(relativePath('./tsc-tests/dist')); removeDirectory(relativePath('./tsc-tests/generated')); } function createGeneratedCodeDir() { fs.mkdirSync(relativePath('./tsc-tests/generated')); } function assertFileExists(relPath) { assert.equal(true, fs.existsSync(relativePath(relPath))); } function multiDone(done, count) { return function() { count -= 1; if (count <= 0) { done(); } }; } function runTscCmd(tscCmd) { try { execSync(tscCmd, {cwd: relativePath('./tsc-tests')}); } catch (e) { assert.fail(e.stdout); } } const outputDir = './test/tsc-tests/generated'; // --skipLibCheck is needed because some of our node_modules/ targets es6 but // our test doesn't pass with `--target es6` // TODO: Find out how we can enable --target es6! const tscCompilerOptions = `--allowJs --strict --noImplicitReturns --skipLibCheck`; describe('tsc test01: nested messages', function() { before(function() { cleanup(); createGeneratedCodeDir(); execSync(`protoc -I=./test/protos test01.proto \ --js_out=import_style=commonjs:${outputDir} \ --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:${outputDir}`); }); after(function() { cleanup(); }); it('generated code should exist', function() { assertFileExists('./tsc-tests/generated/test01_pb.js'); assertFileExists('./tsc-tests/generated/test01_pb.d.ts'); }); it('tsc should run and export', function() { runTscCmd(`tsc client01.ts generated/test01_pb.d.ts generated/test01_pb.js \ ${tscCompilerOptions} --outDir ./dist`); // check for the tsc output assertFileExists('./tsc-tests/dist/client01.js'); assertFileExists('./tsc-tests/dist/generated/test01_pb.js'); // load the compiled js files and do some tests const {msgOuter} = require(relativePath('./tsc-tests/dist/client01.js')); assert.equal(123, msgOuter.getSomepropList()[0].getValue()); }); }); describe('tsc test02: simple rpc, messages in separate proto', function() { before(function() { cleanup(); createGeneratedCodeDir(); MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr(); global.XMLHttpRequest = MockXMLHttpRequest; execSync(`protoc -I=./test/protos test02.proto test03.proto \ --js_out=import_style=commonjs:${outputDir} \ --grpc-web_out=import_style=typescript,mode=grpcwebtext:${outputDir}`); }); after(function() { cleanup(); }); it('generated code should exist', function() { assertFileExists('./tsc-tests/generated/Test02ServiceClientPb.ts'); assertFileExists('./tsc-tests/generated/test02_pb.js'); assertFileExists('./tsc-tests/generated/test02_pb.d.ts'); assertFileExists('./tsc-tests/generated/test03_pb.js'); assertFileExists('./tsc-tests/generated/test03_pb.d.ts'); }); it('tsc should run and export', function(done) { runTscCmd(`tsc client02.ts generated/Test02ServiceClientPb.ts \ generated/test02_pb.d.ts generated/test02_pb.js \ generated/test03_pb.d.ts generated/test03_pb.js \ ${tscCompilerOptions} --outDir ./dist`); // check for the tsc output assertFileExists('./tsc-tests/dist/client02.js'); assertFileExists('./tsc-tests/dist/generated/Test02ServiceClientPb.js'); assertFileExists('./tsc-tests/dist/generated/test02_pb.js'); assertFileExists('./tsc-tests/dist/generated/test03_pb.js'); // load the compiled js files and do some tests MockXMLHttpRequest.onSend = function(xhr) { assert.equal('http://mydummy.com/MyService/addOne', xhr.url); assert.equal('AAAAAAA=', xhr.body); done(); }; require(relativePath('./tsc-tests/dist/client02.js')); }); }); describe('tsc test03: streamInterceptor', function() { before(function() { cleanup(); createGeneratedCodeDir(); MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr(); global.XMLHttpRequest = MockXMLHttpRequest; const genCmd = `protoc -I=./test/protos echo.proto \ --js_out=import_style=commonjs:${outputDir} \ --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:${outputDir}`; execSync(genCmd); }); after(function() { cleanup(); }); it('generated code should exist', function() { assertFileExists('./tsc-tests/generated/echo_pb.js'); assertFileExists('./tsc-tests/generated/echo_pb.d.ts'); assertFileExists('./tsc-tests/generated/echo_grpc_web_pb.js'); assertFileExists('./tsc-tests/generated/echo_grpc_web_pb.d.ts'); }); it('tsc should run and export', function(done) { done = multiDone(done, 3); const tscCmd = `tsc client03.ts \ generated/echo_pb.d.ts generated/echo_pb.js \ generated/echo_grpc_web_pb.d.ts generated/echo_grpc_web_pb.js \ ${tscCompilerOptions} --outDir ./dist`; runTscCmd(tscCmd); // check for the tsc output assertFileExists('./tsc-tests/dist/client03.js'); assertFileExists('./tsc-tests/dist/generated/echo_pb.js'); const {echoService, EchoRequest} = require(relativePath('./tsc-tests/dist/client03.js')); assert.equal('function', typeof echoService.echo); assert.equal('function', typeof echoService.serverStreamingEcho); const req = new EchoRequest(); req.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { // The interceptor will attach "[-out-]" in front of our proto message. // See the interceptor code in client03.ts. // So by the time the proto is being sent by the underlying transport, it // should contain the string "[-out-]aaa". assert.equal('AAAAAAwKClstb3V0LV1hYWE=', xhr.body); xhr.respond(200, {'Content-Type': 'application/grpc-web-text', 'p': 'q'}, // add a piece of initial metadata // echo it back, plus a trailing metadata "x: y" 'AAAAAAwKClstb3V0LV1hYWGAAAAABng6IHkNCg=='); }; // this is the callback-based client var call = echoService.echo(req, {}, (err, response) => { assert.ifError(err); // Now, the interceptor will be invoked again on receiving the response // from the server. It attaches an additional "[-in-]" string in front of // the server response. assert.equal('[-in-][-out-]aaa', response.getMessage()); done(); }); call.on('metadata', (initialMetadata) => { assert('p' in initialMetadata); assert(!('x' in initialMetadata)); assert.equal('q', initialMetadata['p']); done(); }); call.on('status', (status) => { assert('metadata' in status); var trailingMetadata = status.metadata; assert('x' in trailingMetadata); assert(!('p' in trailingMetadata)); assert.equal('y', trailingMetadata['x']); done(); }); }); }); describe('tsc test04: unaryInterceptor', function() { before(function() { cleanup(); createGeneratedCodeDir(); MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr(); global.XMLHttpRequest = MockXMLHttpRequest; const genCmd = `protoc -I=./test/protos echo.proto \ --js_out=import_style=commonjs:${outputDir} \ --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:${outputDir}`; execSync(genCmd); }); after(function() { cleanup(); }); it('generated code should exist', function() { assertFileExists('./tsc-tests/generated/echo_pb.js'); assertFileExists('./tsc-tests/generated/echo_pb.d.ts'); assertFileExists('./tsc-tests/generated/echo_grpc_web_pb.js'); assertFileExists('./tsc-tests/generated/echo_grpc_web_pb.d.ts'); }); it('tsc should run and export', function(done) { const tscCmd = `tsc client04.ts \ generated/echo_pb.d.ts generated/echo_pb.js \ generated/echo_grpc_web_pb.d.ts generated/echo_grpc_web_pb.js \ ${tscCompilerOptions} --outDir ./dist`; runTscCmd(tscCmd); // check for the tsc output assertFileExists('./tsc-tests/dist/client04.js'); assertFileExists('./tsc-tests/dist/generated/echo_pb.js'); const {echoService, EchoRequest} = require(relativePath('./tsc-tests/dist/client04.js')); assert.equal('function', typeof echoService.echo); assert.equal('function', typeof echoService.serverStreamingEcho); const req = new EchoRequest(); req.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { // The interceptor will attach "[-out-]" in front of our proto message. // See the interceptor code in client04.ts. // So by the time the proto is being sent by the underlying transport, it // should contain the string "[-out-]aaa". assert.equal('AAAAAAwKClstb3V0LV1hYWE=', xhr.body); xhr.respond(200, {'Content-Type': 'application/grpc-web-text', 'p': 'q'}, // add a piece of initial metadata // echo it back, plus a trailing metadata "x: y" 'AAAAAAwKClstb3V0LV1hYWGAAAAABng6IHkNCg=='); }; // this is the promise-based client echoService.echo(req, {}).then((response) => { // Now, the interceptor will be invoked again on receiving the response // from the server. See the initerceptor logic in client04.ts. It // flattens both the initialMetadata and the trailingMetadata, and then // attaches an additional "[-in-]" string in front of the server // response. assert.equal('<-InitialMetadata->p: q<-TrailingMetadata->x: y'+ '[-in-][-out-]aaa', response.getMessage()); done(); }); }); }); describe('tsc test05: callback-based client', function() { before(function() { cleanup(); createGeneratedCodeDir(); MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr(); global.XMLHttpRequest = MockXMLHttpRequest; const genCmd = `protoc -I=./test/protos echo.proto \ --js_out=import_style=commonjs:${outputDir} \ --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:${outputDir}`; execSync(genCmd); }); after(function() { cleanup(); }); it('generated code should exist', function() { assertFileExists('./tsc-tests/generated/echo_pb.js'); assertFileExists('./tsc-tests/generated/echo_pb.d.ts'); assertFileExists('./tsc-tests/generated/echo_grpc_web_pb.js'); assertFileExists('./tsc-tests/generated/echo_grpc_web_pb.d.ts'); }); it('tsc should run and export', function() { const tscCmd = `tsc client05.ts \ generated/echo_pb.d.ts generated/echo_pb.js \ generated/echo_grpc_web_pb.d.ts generated/echo_grpc_web_pb.js \ ${tscCompilerOptions} --outDir ./dist`; // this test only makes sure the TS client code compiles successfully runTscCmd(tscCmd); // check for the tsc output assertFileExists('./tsc-tests/dist/client05.js'); assertFileExists('./tsc-tests/dist/generated/echo_pb.js'); }); }); describe('tsc test06: promise-based client', function() { before(function() { cleanup(); createGeneratedCodeDir(); MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr(); global.XMLHttpRequest = MockXMLHttpRequest; const genCmd = `protoc -I=./test/protos echo.proto \ --js_out=import_style=commonjs:${outputDir} \ --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:${outputDir}`; execSync(genCmd); }); after(function() { cleanup(); }); it('generated code should exist', function() { assertFileExists('./tsc-tests/generated/echo_pb.js'); assertFileExists('./tsc-tests/generated/echo_pb.d.ts'); assertFileExists('./tsc-tests/generated/echo_grpc_web_pb.js'); assertFileExists('./tsc-tests/generated/echo_grpc_web_pb.d.ts'); }); it('tsc should run and export', function() { const tscCmd = `tsc client06.ts \ generated/echo_pb.d.ts generated/echo_pb.js \ generated/echo_grpc_web_pb.d.ts generated/echo_grpc_web_pb.js \ ${tscCompilerOptions} --outDir ./dist`; // this test only makes sure the TS client code compiles successfully runTscCmd(tscCmd); // check for the tsc output assertFileExists('./tsc-tests/dist/client06.js'); assertFileExists('./tsc-tests/dist/generated/echo_pb.js'); }); }); ================================================ FILE: scripts/README.md ================================================ # Scripts A collection of scripts (mostly test related). ## Troubleshooting - Bazel Crashes (OOM) [Bazel](https://github.com/bazelbuild/bazel) can be memory hungry and often crashes while building on Mac with default Docker memory settings (2GB) (similar reports: [here](https://github.com/tensorflow/models/issues/3647) and [here](https://stackoverflow.com/questions/65605663/cannot-build-with-error-server-terminated-abruptly)). Bump the [memory settings](https://docs.docker.com/docker-for-mac/#resources) in Docker Desktop for Mac (e.g. to 4 - 6GB) if you see the following errors: ``` $ bazel build //javascript/net/grpc/web/generator/... //net/grpc/gateway/examples/echo/... ... Server terminated abruptly (error code: 14, error message: 'Socket closed', log file: '/root/.cache/bazel/_bazel_root/.../server/jvm.out') ------ failed to solve: rpc error: code = Unknown desc = executor failed running [/bin/sh -c bazel build javascript/net/grpc/web/... && cp $(bazel info bazel-genfiles)/javascript/net/grpc/web/protoc-gen-grpc-web /usr/local/bin/protoc-gen-grpc-web]: exit code: 37 ``` ================================================ FILE: scripts/docker-run-build-tests.sh ================================================ #!/bin/bash # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex # This script is intended to be run within the base image from # net/grpc/gateway/docker/prereqs/Dockerfile # Ensures all Bazel targets builds cd /github/grpc-web && \ bazel clean && \ bazel build \ //javascript/net/grpc/web/generator/... \ //net/grpc/gateway/examples/echo/... ================================================ FILE: scripts/docker-run-interop-tests.sh ================================================ #!/bin/bash # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex # This script is intended to be run within the base image from # net/grpc/gateway/docker/prereqs/Dockerfile cd /github/grpc-web/test/interop && \ npm install && \ npm link grpc-web # Test grpc-web-text mode protoc -I=../.. src/proto/grpc/testing/test.proto \ src/proto/grpc/testing/empty.proto \ src/proto/grpc/testing/messages.proto \ --js_out=import_style=commonjs:. \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:. && \ npm test # Test grpc-web mode (binary) protoc -I=../.. src/proto/grpc/testing/test.proto \ src/proto/grpc/testing/empty.proto \ src/proto/grpc/testing/messages.proto \ --js_out=import_style=commonjs:. \ --grpc-web_out=import_style=commonjs,mode=grpcweb:. && \ npm test -- --mode=binary ================================================ FILE: scripts/docker-run-jsunit-tests.sh ================================================ #!/bin/bash # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex # This script is intended to be run within the base image from # packages/grpc-web/docker/jsunit-test/Dockerfile cd /grpc-web/packages/grpc-web npm run test-jsunit ================================================ FILE: scripts/docker-run-mocha-tests.sh ================================================ #!/bin/bash # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex # This script is intended to be run within the base image from # net/grpc/gateway/docker/prereqs/Dockerfile cd /github/grpc-web/packages/grpc-web npm run prepare && \ npm run test-mocha ================================================ FILE: scripts/init_submodules.sh ================================================ #!/bin/bash # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex cd "$(dirname "$0")"/.. git submodule --quiet update --init --recursive (cd third_party/protobuf && git checkout tags/v3.15.6) ================================================ FILE: scripts/kokoro.sh ================================================ #!/bin/bash # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex SCRIPT_DIR=$(dirname "$0") cd "${SCRIPT_DIR}" export MASTER=1 ./run_basic_tests.sh ./run_interop_tests.sh ================================================ FILE: scripts/release_notes.py ================================================ # Copyright 2020 gRPC authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Generate release notes in Markdown from Github PRs. You'll need a github API token to avoid being rate-limited. See https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ 1. Create a draft release notes / changelog, by running: $ python3 scripts/release_notes.py --token= --unreleased_only 2. Adjust each PR, if necessary, after reading the draft: 2a. Apply one of these labels, if you want the PR to be included in the release notes / changelog: - release notes: breaking for breaking changes - release notes: major for major features - release notes: yes for other fixes and changes 2b. Fix any PR title, to better reflect the changes done. 2c. Add "author:@" to the PR body to attribute the code to the original author if this is an import/export. 3. Finalize the release notes, by running: $ python3 scripts/release_notes.py --token= > CHANGELOG.md 4. Check in the changes. """ from collections import defaultdict import urllib from urllib.request import Request, urlopen from enum import Enum import json import re import subprocess API_BASE_URL = "https://api.github.com/repos/grpc/grpc-web" GRPC_WEB_TEAM = [ "stanley-cheung", "sampajano", "fengli79", "vnorigoog", "wenbozhu", "jtattermusch", "srini100", "hsaliak", ] UNRELEASED = 'Unreleased' class LabelLevel(Enum): NO_LABEL = 0 WITH_LABEL = 1 # release notes: yes MAJOR_FEATURE = 2 # release notes: major BREAKING_CHANGE = 3 # release notes: breaking def __gt__(self, other): if self.__class__ is other.__class__: return self.value > other.value return NotImplemented def __lt__(self, other): if self.__class__ is other.__class__: return self.value < other.value return NotImplemented # Represent the changelog of one release class ReleaseNotes: def __init__(self): self.without_labels = [] self.with_labels = [] self.major_features = [] self.breaking_changes = [] # Main operations class ProcessChangelog: def __init__(self): self.token = "" self.releases = [] self.merged_prs = [] self.changelog_by_release = defaultdict(ReleaseNotes) # When True, only gather and output the Unreleased section and # stop querying older PR pages as soon as we encounter the first # PR that belongs to a released tag. self.unreleased_only = False # Make a Github API call def github_api(self, url): if not url.startswith('http'): url = API_BASE_URL + url req = Request(url) req.add_header('Authorization', 'token {}'.format(self.token)) f = urlopen(req) response = json.loads(f.read().decode('utf-8')) return response, f.info() # Find out which release a PR belongs to def check_release(self, sha): for release in self.releases: retcode = subprocess.call([ "git", "merge-base", "--is-ancestor", sha, release['sha'] ]) if retcode == 0: return release['release'] return UNRELEASED # A helper function to check through all the "release notes: xxx" # labels of a PR and return the highest level def _get_pr_label_level(self, labels): label_level = LabelLevel.NO_LABEL for label in labels: _label_level = LabelLevel.NO_LABEL if label['name'] == "release notes: yes": _label_level = LabelLevel.WITH_LABEL elif label['name'] == "release notes: major": _label_level = LabelLevel.MAJOR_FEATURE elif label['name'] == "release notes: breaking": _label_level = LabelLevel.BREAKING_CHANGE if _label_level > label_level: # retain the highest level label_level = _label_level return label_level # Retrieve the list of all the releases def get_releases(self): if self.unreleased_only: # Optimization: only need the latest release to detect boundary latest_release, _ = self.github_api('/releases/latest') ref_data, _ = self.github_api('/git/ref/tags/' + latest_release['tag_name']) tag_data, _ = self.github_api('/git/commits/' + ref_data['object']['sha']) self.releases.append({ 'release': latest_release['tag_name'], 'date': tag_data['author']['date'], 'sha': ref_data['object']['sha'][0:7] }) else: # Might get into trouble if we ever have more than 30 releases github_releases, _ = self.github_api('/releases') for release in github_releases: ref_data, _ = self.github_api('/git/ref/tags/' + release['tag_name']) tag_data, _ = self.github_api('/git/commits/' + ref_data['object']['sha']) self.releases.append({ 'release': release['tag_name'], 'date': tag_data['author']['date'], 'sha': ref_data['object']['sha'][0:7] }) self.releases.sort(key=lambda val:val['date']) # Retrieve the list of all merged PRs def get_merged_prs(self, num_pages): url = '/pulls?state=closed' while True: response, headers = self.github_api(url) for pr in response: if pr['number'] == 1: # had trouble with git merge-base continue if pr['merged_at'] is None: # not merged continue label_level = self._get_pr_label_level(pr['labels']) m = None if pr['body'] is None else re.search( r'author: ?@([A-Za-z\d-]+)', pr['body']) if m: author = m[1] # author attribution override else: author = pr['user']['login'] sha = pr['merge_commit_sha'][0:7] release = self.check_release(sha) self.merged_prs.append({ 'number': str(pr['number']), 'author': author, 'title': pr['title'], 'release': release, 'label_level': label_level, }) # Optimization: if only unreleased requested and we hit # a PR that's already part of a released tag, we can # stop. The API returns PRs in reverse chronological # order, so older ones will also be released. if self.unreleased_only and release != UNRELEASED: return num_pages -= 1 if num_pages == 0: break link = headers.get('link') if re.search(r'; rel="next"', link): # next page url = re.sub(r'.*<(.*)>; rel="next".*', r'\1', link) else: break # Format each PR into one line of release notes, and classify them # by release and label level def format_release_notes(self): for pr in self.merged_prs: release = pr['release'] author = pr['author'] num = '[#{}](https://github.com/grpc/grpc-web/pull/{})'.format( pr['number'], pr['number']) if len(pr['title']) > 70: title = pr['title'][0:70] + "..." else: title = pr['title'] if author not in GRPC_WEB_TEAM: credit = f" @{author}" else: credit = "" final_formatted_line = "- {} {}{}".format(num, title, credit) release_notes = self.changelog_by_release[release] if pr['label_level'] == LabelLevel.BREAKING_CHANGE: release_notes.breaking_changes.append(final_formatted_line) elif pr['label_level'] == LabelLevel.MAJOR_FEATURE: release_notes.major_features.append(final_formatted_line) elif pr['label_level'] == LabelLevel.WITH_LABEL: release_notes.with_labels.append(final_formatted_line) else: release_notes.without_labels.append(final_formatted_line) # Print the final result in the form of CHANGELOG.md def print_changelog(self, output_without_labels, output_unreleased_only): print("[//]: # (GENERATED FILE -- DO NOT EDIT!)") print("[//]: # (See scripts/release_notes.py for more details.)") for release, release_notes in self.changelog_by_release.items(): # Always include Unreleased unless we're filtering to only # unreleased (handled by the check below). if output_unreleased_only and release != UNRELEASED: continue print_other_changes_heading = False print("") print("## {}".format(release)) if release_notes.breaking_changes: print_other_changes_heading = True print("") print("### Breaking Changes") print("") for line in release_notes.breaking_changes: print(line) if release_notes.major_features: print_other_changes_heading = True print("") print("### Major Features") print("") for line in release_notes.major_features: print(line) if release_notes.with_labels: print("") if print_other_changes_heading: print("### Other Changes") print("") for line in release_notes.with_labels: print(line) if release_notes.without_labels and output_without_labels: print("") print("### Without Labels") print("") for line in release_notes.without_labels: print(line) print("") def build_args_parser(): import argparse parser = argparse.ArgumentParser() parser.add_argument('--token', type=str, default='', help='Github API token') parser.add_argument('--num_pages', type=int, default=20, help='Number of pages to go back') parser.add_argument('--output_without_labels', default=False, action='store_true', help='Whether to output PRs without labels') parser.add_argument('--unreleased_only', default=False, action='store_true', help='Only output the Unreleased section (Including PRs without labels)') return parser def main(): parser = build_args_parser() args = parser.parse_args() token, num_pages = args.token, args.num_pages unreleased_only = args.unreleased_only # If --unreleased_only is set, we implicitly enable without-labels output. output_without_labels = args.output_without_labels or unreleased_only if token == "": print("Error: Github API token is required --token=") return worker = ProcessChangelog() worker.token = token worker.unreleased_only = unreleased_only # Retrieve the list of all the releases (optimized when unreleased_only) worker.get_releases() # Retrieve the list of all merged PRs worker.get_merged_prs(num_pages) # Format each PR into one line of release notes, and classify them # by release and label level worker.format_release_notes() # Print the final result in the form of CHANGELOG.md worker.print_changelog(output_without_labels, unreleased_only) if __name__ == "__main__": main() ================================================ FILE: scripts/run_basic_tests.sh ================================================ #!/bin/bash # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex SCRIPT_DIR=$(dirname "$0") REPO_DIR=$(realpath "${SCRIPT_DIR}/..") # Set up cd "${REPO_DIR}" # These programs need to be already installed progs=(docker docker-compose curl) for p in "${progs[@]}" do command -v "$p" > /dev/null 2>&1 || \ { echo >&2 "$p is required but not installed. Aborting."; exit 1; } done ########################################################## # Step 1: Run all unit tests ########################################################## echo -e "\n[Running] Basic test #1 - Runnning unit tests" # Run jsunit tests docker-compose build jsunit-test docker run --rm grpcweb/jsunit-test /bin/bash \ /grpc-web/scripts/docker-run-jsunit-tests.sh # Run (mocha) unit tests docker-compose build prereqs docker run --rm grpcweb/prereqs /bin/bash \ /github/grpc-web/scripts/docker-run-mocha-tests.sh ########################################################## # Step 2: Test echo server ########################################################## echo -e "\n[Running] Basic test #2 - Testing echo server" docker-compose build prereqs envoy node-server # Bring up the Echo server and the Envoy proxy (in background). # The 'sleep' seems necessary for the docker containers to be fully up # and listening before we test the with curl requests docker-compose up -d node-server envoy && sleep 5; # Run a curl request and verify the output source ./scripts/test-proxy.sh # Remove all docker containers docker-compose down ########################################################## # Step 3: Test all Dockerfile and Bazel targets can build! ########################################################## echo -e "\n[Running] Basic test #3 - Testing everything buids" if [[ "$MASTER" == "1" ]]; then # Build all for continuous_integration docker-compose build else # Only build a subset of docker images for presubmit runs docker-compose build commonjs-client closure-client ts-client fi # Run build tests to ensure all Bazel targets can build. docker run --rm grpcweb/prereqs /bin/bash \ /github/grpc-web/scripts/docker-run-build-tests.sh # Clean up git clean -f -d -x echo 'Basic tests completed successfully!' ================================================ FILE: scripts/run_interop_tests.sh ================================================ #!/bin/bash # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex run_tests () { docker run --network=host --rm grpcweb/prereqs /bin/bash \ /github/grpc-web/scripts/docker-run-interop-tests.sh } SCRIPT_DIR=$(dirname "$0") REPO_DIR=$(realpath "${SCRIPT_DIR}/..") # Set up cd "${REPO_DIR}" # These programs need to be already installed progs=(docker docker-compose npm) for p in "${progs[@]}" do command -v "$p" > /dev/null 2>&1 || \ { echo >&2 "$p is required but not installed. Aborting."; exit 1; } done function cleanup () { echo "Killing lingering Docker servers..." docker rm -f "$pid1" docker rm -f "$pid2" } trap cleanup EXIT # Build all relevant docker images. They should all build successfully. docker-compose build prereqs node-interop-server ########################################################## # Run interop tests (against Envoy) ########################################################## echo -e "\n[Running] Interop test (Envoy)" pid1=$(docker run -d \ -v "$(pwd)"/test/interop/envoy.yaml:/etc/envoy/envoy.yaml:ro \ --network=host envoyproxy/envoy:v1.22.0) pid2=$(docker run -d --network=host grpcweb/node-interop-server) run_tests docker rm -f "$pid1" docker rm -f "$pid2" ================================================ FILE: scripts/test-proxy.sh ================================================ #!/bin/bash # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex # Run a curl request to test the output of the proxy and the backend server. # This is a simple unary call with "hello" as the protobuf message out=$(curl -s 'http://localhost:8080/grpc.gateway.testing.EchoService/Echo' \ -H 'Content-Type: application/grpc-web-text' \ -H 'Accept: application/grpc-web-text' \ -H 'Connection: keep-alive' \ -H 'X-Grpc-Web: 1' \ -H 'X-User-Agent: grpc-web-javascript/0.1' \ --data-binary 'AAAAAAcKBWhlbGxv') # Cut out a few parts of the response that we are reasonably sure that should # not change. # # Take the first 13 bytes: # First byte: 00 (data marker) # Next 4 bytes: 00 00 00 07 (length of payload) # Next 7 bytes: 0a 05 68 65 6c 6c 6f (binary proto of "1: hello") # Next 1 byte: 80 (trailer marker) # Skip the next 4 bytes: # This represents the length of the trailer frame, which could be unreliable. # Take the next 15 bytes: # This is the beginning of the trailer frame, which we are reasonably sure # that it will begin with: # grpc-status:0\r\n s1=$(echo "$out" | base64 -d | \ { dd bs=1 count=13 ; dd skip=4 bs=1 count=15 ; } 2>/dev/null | \ base64) echo "$s1" | base64 -d | xxd # Take the 28 bytes we cut out above, the base64-encoded string should be this if [[ "$s1" != "AAAAAAcKBWhlbGxvgGdycGMtc3RhdHVzOjANCg==" ]]; then exit 1; else echo "Envoy proxy test successful!" fi ================================================ FILE: src/proto/grpc/testing/empty.proto ================================================ // Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package grpc.testing; // An empty message that you can re-use to avoid defining duplicated empty // messages in your project. A typical example is to use it as argument or the // return value of a service API. For instance: // // service Foo { // rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; // }; // message Empty {} ================================================ FILE: src/proto/grpc/testing/messages.proto ================================================ // Copyright 2015-2016 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Message definitions to be used by integration test service definitions. syntax = "proto3"; package grpc.testing; // TODO(dgq): Go back to using well-known types once // https://github.com/grpc/grpc/issues/6980 has been fixed. // import "google/protobuf/wrappers.proto"; message BoolValue { // The bool value. bool value = 1; } // The type of payload that should be returned. enum PayloadType { // Compressable text format. COMPRESSABLE = 0; } // A block of data, to simply increase gRPC message size. message Payload { // The type of data in body. PayloadType type = 1; // Primary contents of payload. bytes body = 2; } // A protobuf representation for grpc status. This is used by test // clients to specify a status that the server should attempt to return. message EchoStatus { int32 code = 1; string message = 2; } // The type of route that a client took to reach a server w.r.t. gRPCLB. // The server must fill in "fallback" if it detects that the RPC reached // the server via the "gRPCLB fallback" path, and "backend" if it detects // that the RPC reached the server via "gRPCLB backend" path (i.e. if it got // the address of this server from the gRPCLB server BalanceLoad RPC). Exactly // how this detection is done is context and server dependent. enum GrpclbRouteType { // Server didn't detect the route that a client took to reach it. GRPCLB_ROUTE_TYPE_UNKNOWN = 0; // Indicates that a client reached a server via gRPCLB fallback. GRPCLB_ROUTE_TYPE_FALLBACK = 1; // Indicates that a client reached a server as a gRPCLB-given backend. GRPCLB_ROUTE_TYPE_BACKEND = 2; } // Unary request. message SimpleRequest { // Desired payload type in the response from the server. // If response_type is RANDOM, server randomly chooses one from other formats. PayloadType response_type = 1; // Desired payload size in the response from the server. int32 response_size = 2; // Optional input payload sent along with the request. Payload payload = 3; // Whether SimpleResponse should include username. bool fill_username = 4; // Whether SimpleResponse should include OAuth scope. bool fill_oauth_scope = 5; // Whether to request the server to compress the response. This field is // "nullable" in order to interoperate seamlessly with clients not able to // implement the full compression tests by introspecting the call to verify // the response's compression status. BoolValue response_compressed = 6; // Whether server should return a given status EchoStatus response_status = 7; // Whether the server should expect this request to be compressed. BoolValue expect_compressed = 8; // Whether SimpleResponse should include server_id. bool fill_server_id = 9; // Whether SimpleResponse should include grpclb_route_type. bool fill_grpclb_route_type = 10; } // Unary response, as configured by the request. message SimpleResponse { // Payload to increase message size. Payload payload = 1; // The user the request came from, for verifying authentication was // successful when the client expected it. string username = 2; // OAuth scope. string oauth_scope = 3; // Server ID. This must be unique among different server instances, // but the same across all RPC's made to a particular server instance. string server_id = 4; // gRPCLB Path. GrpclbRouteType grpclb_route_type = 5; // Server hostname. string hostname = 6; } // Client-streaming request. message StreamingInputCallRequest { // Optional input payload sent along with the request. Payload payload = 1; // Whether the server should expect this request to be compressed. This field // is "nullable" in order to interoperate seamlessly with servers not able to // implement the full compression tests by introspecting the call to verify // the request's compression status. BoolValue expect_compressed = 2; // Not expecting any payload from the response. } // Client-streaming response. message StreamingInputCallResponse { // Aggregated size of payloads received from the client. int32 aggregated_payload_size = 1; } // Configuration for a particular response. message ResponseParameters { // Desired payload sizes in responses from the server. int32 size = 1; // Desired interval between consecutive responses in the response stream in // microseconds. int32 interval_us = 2; // Whether to request the server to compress the response. This field is // "nullable" in order to interoperate seamlessly with clients not able to // implement the full compression tests by introspecting the call to verify // the response's compression status. BoolValue compressed = 3; } // Server-streaming request. message StreamingOutputCallRequest { // Desired payload type in the response from the server. // If response_type is RANDOM, the payload from each response in the stream // might be of different types. This is to simulate a mixed type of payload // stream. PayloadType response_type = 1; // Configuration for each expected response message. repeated ResponseParameters response_parameters = 2; // Optional input payload sent along with the request. Payload payload = 3; // Whether server should return a given status EchoStatus response_status = 7; } // Server-streaming response, as configured by the request and parameters. message StreamingOutputCallResponse { // Payload to increase response size. Payload payload = 1; } // For reconnect interop test only. // Client tells server what reconnection parameters it used. message ReconnectParams { int32 max_reconnect_backoff_ms = 1; } // For reconnect interop test only. // Server tells client whether its reconnects are following the spec and the // reconnect backoffs it saw. message ReconnectInfo { bool passed = 1; repeated int32 backoff_ms = 2; } message LoadBalancerStatsRequest { // Request stats for the next num_rpcs sent by client. int32 num_rpcs = 1; // If num_rpcs have not completed within timeout_sec, return partial results. int32 timeout_sec = 2; } message LoadBalancerStatsResponse { // The number of completed RPCs for each peer. map rpcs_by_peer = 1; // The number of RPCs that failed to record a remote peer. int32 num_failures = 2; } ================================================ FILE: src/proto/grpc/testing/test.proto ================================================ // Copyright 2015-2016 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // An integration test service that covers all the method signature permutations // of unary/streaming requests/responses. syntax = "proto3"; import "src/proto/grpc/testing/empty.proto"; import "src/proto/grpc/testing/messages.proto"; package grpc.testing; // A simple service to test the various types of RPCs and experiment with // performance with various types of payload. service TestService { // One empty request followed by one empty response. rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty); // One request followed by one response. rpc UnaryCall(SimpleRequest) returns (SimpleResponse); // One request followed by one response. Response has cache control // headers set such that a caching HTTP proxy (such as GFE) can // satisfy subsequent requests. rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); // One request followed by a sequence of responses (streamed download). // The server returns the payload with client desired type and sizes. rpc StreamingOutputCall(StreamingOutputCallRequest) returns (stream StreamingOutputCallResponse); // A sequence of requests followed by one response (streamed upload). // The server returns the aggregated size of client payload as the result. rpc StreamingInputCall(stream StreamingInputCallRequest) returns (StreamingInputCallResponse); // A sequence of requests with each request served by the server immediately. // As one request could lead to multiple responses, this interface // demonstrates the idea of full duplexing. rpc FullDuplexCall(stream StreamingOutputCallRequest) returns (stream StreamingOutputCallResponse); // A sequence of requests followed by a sequence of responses. // The server buffers all the client requests and then serves them in order. A // stream of responses are returned to the client when the server starts with // first request. rpc HalfDuplexCall(stream StreamingOutputCallRequest) returns (stream StreamingOutputCallResponse); // The test server will not implement this method. It will be used // to test the behavior when clients call unimplemented methods. rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); } // A simple service NOT implemented at servers so clients can test for // that case. service UnimplementedService { // A call that no server should implement rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); } // A service used to control reconnect server. service ReconnectService { rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty); rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo); } // A service used to obtain stats for verifying LB behavior. service LoadBalancerStatsService { // Gets the backend distribution for RPCs sent by a test client. rpc GetClientStats(LoadBalancerStatsRequest) returns (LoadBalancerStatsResponse) {} } ================================================ FILE: test/interop/.gitignore ================================================ node_modules/ package-lock.json src/ ================================================ FILE: test/interop/README.md ================================================ gRPC-Web Interop Tests ====================== See the [main doc](https://github.com/grpc/grpc-web/blob/master/doc/interop-test-descriptions.md) for details about gRPC interop tests in general and the list of test cases. Run interop tests ----------------- ### Build some docker images ```sh $ cd grpc-web $ docker-compose build prereqs node-interop-server interop-client ``` ### Run the Node interop server An interop server implemented in Node is hosted in the `grpc/grpc-node` repo. ```sh $ docker run -d --network=host grpcweb/node-interop-server ``` ### Run the Envoy proxy An `envoy.yaml` file is provided in this directory to direct traffic for these tests. ```sh $ docker run -d -v $(pwd)/test/interop/envoy.yaml:/etc/envoy/envoy.yaml:ro \ --network=host envoyproxy/envoy:v1.22.0 ``` ### Run the gRPC-Web browser client You can either run the interop client as `npm test`, like this: ```sh $ docker run --network=host --rm grpcweb/prereqs /bin/bash \ /github/grpc-web/scripts/docker-run-interop-tests.sh ``` Or from the browser: ```sh $ docker-compose up interop-client ``` Open up the browser and go to `http://localhost:8081/index.html` and open up the console. ================================================ FILE: test/interop/envoy.yaml ================================================ admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: interop_service timeout: 0s max_stream_duration: grpc_timeout_header_max: 0s cors: allow_origin_string_match: - prefix: "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-grpc-test-echo-initial,x-grpc-test-echo-trailing-bin,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout max_age: "1728000" expose_headers: x-grpc-test-echo-initial,x-grpc-test-echo-trailing-bin,grpc-status,grpc-message http_filters: - name: envoy.filters.http.grpc_web typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - name: envoy.filters.http.cors typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: interop_service connect_timeout: 0.25s type: logical_dns # HTTP/2 support typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} lb_policy: round_robin load_assignment: cluster_name: cluster_0 endpoints: - lb_endpoints: - endpoint: address: socket_address: address: localhost port_value: 7074 ================================================ FILE: test/interop/index.html ================================================ Interop Test

Please open up the console to see the test results.

================================================ FILE: test/interop/interop_client.js ================================================ /** * * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const {Empty} = require('./src/proto/grpc/testing/empty_pb.js'); const {SimpleRequest, StreamingOutputCallRequest, EchoStatus, Payload, ResponseParameters} = require('./src/proto/grpc/testing/messages_pb.js'); const {TestServiceClient} = require('./src/proto/grpc/testing/test_grpc_web_pb.js'); var assert = require('assert'); const grpc = {}; grpc.web = require('grpc-web'); const SERVER_HOST = 'http://localhost:8080'; const TIMEOUT_MS = 1000; // 1 second function multiDone(done, count) { return function() { count -= 1; if (count <= 0) { done(); } }; } function doEmptyUnary(done) { var testService = new TestServiceClient(SERVER_HOST, null, null); testService.emptyCall(new Empty(), null, (err, response) => { assert.ifError(err); assert(response instanceof Empty); done(); }); } function doEmptyUnaryWithDeadline(done) { var testService = new TestServiceClient(SERVER_HOST, null, null); const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 1); testService.emptyCall(new Empty(), {deadline: deadline.getTime().toString()}, (err, response) => { assert.ifError(err); assert(response instanceof Empty); done(); }); } function doLargeUnary(done) { var testService = new TestServiceClient(SERVER_HOST, null, null); var req = new SimpleRequest(); var size = 314159; var payload = new Payload(); payload.setBody('0'.repeat(271828)); req.setPayload(payload); req.setResponseSize(size); testService.unaryCall(req, null, (err, response) => { assert.ifError(err); assert.equal(response.getPayload().getBody().length, size); done(); }); } function doServerStreaming(done) { var testService = new TestServiceClient(SERVER_HOST, null, null); var sizes = [31415, 9, 2653, 58979]; var responseParams = sizes.map((size, idx) => { var param = new ResponseParameters(); param.setSize(size); param.setIntervalUs(idx * 10); return param; }); var req = new StreamingOutputCallRequest(); req.setResponseParametersList(responseParams); var stream = testService.streamingOutputCall(req); done = multiDone(done, sizes.length); var numCallbacks = 0; stream.on('data', (response) => { assert.equal(response.getPayload().getBody().length, sizes[numCallbacks]); numCallbacks++; done(); }); } function doCustomMetadata(done) { var testService = new TestServiceClient(SERVER_HOST, null, null); done = multiDone(done, 3); var req = new SimpleRequest(); const size = 314159; const ECHO_INITIAL_KEY = 'x-grpc-test-echo-initial'; const ECHO_INITIAL_VALUE = 'test_initial_metadata_value'; const ECHO_TRAILING_KEY = 'x-grpc-test-echo-trailing-bin'; const ECHO_TRAILING_VALUE = 0xababab; var payload = new Payload(); payload.setBody('0'.repeat(271828)); req.setPayload(payload); req.setResponseSize(size); var call = testService.unaryCall(req, { [ECHO_INITIAL_KEY]: ECHO_INITIAL_VALUE, [ECHO_TRAILING_KEY]: ECHO_TRAILING_VALUE }, (err, response) => { assert.ifError(err); assert.equal(response.getPayload().getBody().length, size); done(); }); call.on('metadata', (metadata) => { assert(ECHO_INITIAL_KEY in metadata); assert.equal(metadata[ECHO_INITIAL_KEY], ECHO_INITIAL_VALUE); done(); }); call.on('status', (status) => { assert('metadata' in status); assert(ECHO_TRAILING_KEY in status.metadata); assert.equal(status.metadata[ECHO_TRAILING_KEY], ECHO_TRAILING_VALUE); done(); }); } function doStatusCodeAndMessage(done) { var testService = new TestServiceClient(SERVER_HOST, null, null); var req = new SimpleRequest(); const TEST_STATUS_MESSAGE = 'test status message'; const echoStatus = new EchoStatus(); echoStatus.setCode(2); echoStatus.setMessage(TEST_STATUS_MESSAGE); req.setResponseStatus(echoStatus); testService.unaryCall(req, {}, (err, response) => { assert(err); assert('code' in err); assert('message' in err); assert.equal(err.code, 2); assert.equal(err.message, TEST_STATUS_MESSAGE); done(); }); } function doUnimplementedMethod(done) { var testService = new TestServiceClient(SERVER_HOST, null, null); testService.unimplementedCall(new Empty(), {}, (err, response) => { assert(err); assert('code' in err); assert.equal(err.code, 12); done(); }); } var testCases = { 'empty_unary': {testFunc: doEmptyUnary}, 'empty_unary_with_deadline': {testFunc: doEmptyUnaryWithDeadline}, 'large_unary': {testFunc: doLargeUnary}, 'server_streaming': {testFunc: doServerStreaming, skipBinaryMode: true}, 'custom_metadata': {testFunc: doCustomMetadata}, 'status_code_and_message': {testFunc: doStatusCodeAndMessage}, 'unimplemented_method': {testFunc: doUnimplementedMethod} }; if (typeof window === 'undefined') { // Running from Node console.log('Running from Node...'); // Fill in XHR runtime global.XMLHttpRequest = require("xhr2"); var parseArgs = require('minimist'); var argv = parseArgs(process.argv, { string: ['mode'] }); if (argv.mode == 'binary') { console.log('Testing grpc-web mode (binary)...'); } else { console.log('Testing grpc-web-text mode...'); } describe('grpc-web interop tests', function() { Object.keys(testCases).forEach((testCase) => { if (argv.mode == 'binary' && testCases[testCase].skipBinaryMode) return; it('should pass '+testCase, testCases[testCase].testFunc) .timeout(TIMEOUT_MS); }); }); } else { console.log('Running from browser...'); Object.keys(testCases).forEach((testCase) => { var testFunc = testCases[testCase].testFunc; var doneCalled = false; testFunc((err) => { if (err) { throw err; } else { doneCalled = true; console.log(testCase+': passed'); } }); setTimeout(() => { if (!doneCalled) { throw testCase+': failed. Not all done() are called'; } }, TIMEOUT_MS); }); } ================================================ FILE: test/interop/package.json ================================================ { "name": "grpc-web-interop-test", "version": "0.1.0", "description": "gRPC-Web Interop Test Client", "license": "Apache-2.0", "scripts": { "test": "mocha -b interop_client.js" }, "dependencies": { "google-protobuf": "~3.21.4", "grpc-web": "~2.0.2" }, "devDependencies": { "assert": "^2.0.0", "minimist": "~1.2.5", "mocha": "~7.1.1", "webpack": "5.101.3", "webpack-cli": "~5.1.1", "xhr2": "~0.2.0" } } ================================================ FILE: test/interop/webpack.config.js ================================================ module.exports = { mode: "production", entry: "./interop_client.js", };