From 425c9d3f2a0d0fd697e4d78265e3a66157745c2a Mon Sep 17 00:00:00 2001 From: Vladislav Polyakov Date: Sat, 12 Oct 2024 10:42:54 +0300 Subject: [PATCH] refactor: add support user defined UA --- .../src/protocol-grpc/handler-factory.spec.ts | 10 +++--- .../src/protocol-grpc/request-header.spec.ts | 18 ++++++++-- .../src/protocol-grpc/request-header.ts | 34 ++++++++++++++++++- .../connect/src/protocol-grpc/transport.ts | 2 ++ .../connect/src/protocol/transport-options.ts | 9 +++++ 5 files changed, 64 insertions(+), 9 deletions(-) diff --git a/packages/connect/src/protocol-grpc/handler-factory.spec.ts b/packages/connect/src/protocol-grpc/handler-factory.spec.ts index 0e94aee14..9ee846a75 100644 --- a/packages/connect/src/protocol-grpc/handler-factory.spec.ts +++ b/packages/connect/src/protocol-grpc/handler-factory.spec.ts @@ -154,7 +154,7 @@ describe("createHandlerFactory()", function () { const it = res.body![Symbol.asyncIterator](); await it.next(); const writeError = new Error("write error"); - await it.throw?.(writeError).catch(() => {}); + await it.throw?.(writeError).catch(() => { }); await expectAsync(catchError).toBeResolvedTo(writeError); await expectAsync(abortCalled).toBeResolved(); }); @@ -169,7 +169,7 @@ describe("createHandlerFactory()", function () { {}, async (req, ctx) => { handlerContextSignal = ctx.signal; - for (;;) { + for (; ;) { await new Promise((r) => setTimeout(r, 1)); ctx.signal.throwIfAborted(); } @@ -179,7 +179,7 @@ describe("createHandlerFactory()", function () { httpVersion: "2.0", method: "POST", url: `https://example.com/${method.parent.typeName}/${method.name}`, - header: requestHeader(true, timeoutMs, undefined), + header: requestHeader(true, undefined, timeoutMs, undefined), body: createAsyncIterable([encodeEnvelope(0, new Uint8Array(0))]), signal: new AbortController().signal, }); @@ -268,7 +268,7 @@ describe("createHandlerFactory()", function () { {}, async (req, ctx) => { handlerContextSignal = ctx.signal; - for (;;) { + for (; ;) { await new Promise((r) => setTimeout(r, 1)); ctx.signal.throwIfAborted(); } @@ -279,7 +279,7 @@ describe("createHandlerFactory()", function () { httpVersion: "2.0", method: "POST", url: `https://example.com/${method.parent.typeName}/${method.name}`, - header: requestHeader(true, undefined, undefined), + header: requestHeader(true, undefined, undefined, undefined), body: createAsyncIterable([encodeEnvelope(0, new Uint8Array(0))]), signal: ac.signal, }); diff --git a/packages/connect/src/protocol-grpc/request-header.spec.ts b/packages/connect/src/protocol-grpc/request-header.spec.ts index 2cbfac3fe..4cdb5bfb4 100644 --- a/packages/connect/src/protocol-grpc/request-header.spec.ts +++ b/packages/connect/src/protocol-grpc/request-header.spec.ts @@ -27,18 +27,18 @@ function listHeaderKeys(header: Headers): string[] { describe("requestHeader", () => { it("should create request headers", () => { - const headers = requestHeader(true, undefined, undefined); + const headers = requestHeader(true, undefined, undefined, undefined); expect(listHeaderKeys(headers)).toEqual([ "content-type", "te", "user-agent", ]); expect(headers.get("Content-Type")).toBe("application/grpc+proto"); - expect(headers.get("User-Agent")).toMatch(/^connect-es\/\d+\.\d+\.\d+/); + expect(headers.get("User-Agent")).toMatch(/grpc-js-?\w*\//); }); it("should create request headers with timeout", () => { - const headers = requestHeader(true, 10, undefined); + const headers = requestHeader(true, undefined, 10, undefined); expect(listHeaderKeys(headers)).toEqual([ "content-type", "grpc-timeout", @@ -48,6 +48,17 @@ describe("requestHeader", () => { expect(headers.get("Grpc-Timeout")).toBe("10m"); }); + it("should create request headers with userAgent", () => { + const headers = requestHeader(true, "grpc-js-test", 10, undefined); + expect(listHeaderKeys(headers)).toEqual([ + "content-type", + "grpc-timeout", + "te", + "user-agent", + ]); + expect(headers.get("User-Agent")).toBe("grpc-js-test"); + }); + it("should create request headers with compression", () => { const compressionMock: Compression = { name: "gzip", @@ -59,6 +70,7 @@ describe("requestHeader", () => { true, undefined, undefined, + undefined, [compressionMock], compressionMock, ); diff --git a/packages/connect/src/protocol-grpc/request-header.ts b/packages/connect/src/protocol-grpc/request-header.ts index e129f911f..381675711 100644 --- a/packages/connect/src/protocol-grpc/request-header.ts +++ b/packages/connect/src/protocol-grpc/request-header.ts @@ -22,6 +22,31 @@ import { import { contentTypeJson, contentTypeProto } from "./content-type.js"; import type { Compression } from "../protocol/compression.js"; +declare const globalThis: any; + +function getPlatfromUserAgent() { + let runtime = "" + let runtimeVersion = "0.0.0" + + // Bun + if (globalThis.process?.versions?.bun !== undefined) { + runtime = "-bun"; + runtimeVersion = globalThis.process.versions.bun + } + // Deno + else if (globalThis.process?.versions?.deno !== undefined) { + runtime = "-deno"; + runtimeVersion = globalThis.process.versions.deno; + } + // Node + else if (globalThis.process?.versions?.node !== undefined) { + runtime = "-node"; + runtimeVersion = globalThis.process?.versions?.node + } + + return `grpc-js${runtime}/${runtimeVersion} (CONNECT_ES_USER_AGENT)`; +} + /** * Creates headers for a gRPC request. * @@ -29,6 +54,7 @@ import type { Compression } from "../protocol/compression.js"; */ export function requestHeader( useBinaryFormat: boolean, + userAgent: string | undefined, timeoutMs: number | undefined, userProvidedHeaders: HeadersInit | undefined, ): Headers { @@ -38,6 +64,11 @@ export function requestHeader( useBinaryFormat ? contentTypeProto : contentTypeJson, ); + result.set( + headerUserAgent, + userAgent || result.get(headerUserAgent) || getPlatfromUserAgent(), + ); + if (timeoutMs !== undefined) { result.set(headerTimeout, `${timeoutMs}m`); } @@ -55,12 +86,13 @@ export function requestHeader( */ export function requestHeaderWithCompression( useBinaryFormat: boolean, + userAgent: string | undefined, timeoutMs: number | undefined, userProvidedHeaders: HeadersInit | undefined, acceptCompression: Compression[], sendCompression: Compression | null, ): Headers { - const result = requestHeader(useBinaryFormat, timeoutMs, userProvidedHeaders); + const result = requestHeader(useBinaryFormat, userAgent, timeoutMs, userProvidedHeaders); if (sendCompression != null) { result.set(headerEncoding, sendCompression.name); } diff --git a/packages/connect/src/protocol-grpc/transport.ts b/packages/connect/src/protocol-grpc/transport.ts index 0ccd408fb..eafadc84c 100644 --- a/packages/connect/src/protocol-grpc/transport.ts +++ b/packages/connect/src/protocol-grpc/transport.ts @@ -87,6 +87,7 @@ export function createTransport(opt: CommonTransportOptions): Transport { url: createMethodUrl(opt.baseUrl, method), header: requestHeaderWithCompression( opt.useBinaryFormat, + opt.userAgent, timeoutMs, header, opt.acceptCompression, @@ -205,6 +206,7 @@ export function createTransport(opt: CommonTransportOptions): Transport { url: createMethodUrl(opt.baseUrl, method), header: requestHeaderWithCompression( opt.useBinaryFormat, + opt.userAgent, timeoutMs, header, opt.acceptCompression, diff --git a/packages/connect/src/protocol/transport-options.ts b/packages/connect/src/protocol/transport-options.ts index d6a8dea7e..81ce55f75 100644 --- a/packages/connect/src/protocol/transport-options.ts +++ b/packages/connect/src/protocol/transport-options.ts @@ -55,6 +55,15 @@ export interface CommonTransportOptions { */ interceptors: Interceptor[]; + /** + * The user agent to use for all HTTP requests. + * + * https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#user-agents + * + * Example: `userAgent: "grpc-node-js/22.0.0 (connect-es/1.0.0; typescript/5.0.0)"` + */ + userAgent?: string; + /** * Options for the JSON format. * By default, unknown fields are ignored.