From a448dd5b4b500e8632fe78e8549aa9c04d001222 Mon Sep 17 00:00:00 2001 From: hezhiqing Date: Wed, 19 Aug 2026 16:54:32 +0800 Subject: [PATCH] fix: wrap GET input params in tRPC v11 { json } envelope --- src/client.ts | 7 +++++- tests/client.test.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index dc31d14..0ac9e38 100644 --- a/src/client.ts +++ b/src/client.ts @@ -98,8 +98,13 @@ export async function apiGet( params?: Record, ) { const client = createClient(); + // tRPC v11 servers require the GET `input` query param to be wrapped in a + // { json: ... } envelope (see https://trpc.io/docs/client/http-link). A + // bare object is deserialized as `undefined` and every GET call fails with + // HTTP 400. The envelope is also accepted by older tRPC v10 servers, so it + // works against all Dokploy server versions. const query = params - ? `?input=${encodeURIComponent(JSON.stringify(params))}` + ? `?input=${encodeURIComponent(JSON.stringify({ json: params }))}` : ""; const response = await client.get(`/trpc/${endpoint}${query}`); return response.data?.result?.data?.json ?? response.data; diff --git a/tests/client.test.ts b/tests/client.test.ts index 849213f..e67ef11 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -1,5 +1,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +vi.mock("axios", () => { + return { + default: { + create: () => ({ + get: () => Promise.resolve({ data: {} }), + post: () => Promise.resolve({ data: {} }), + }), + }, + }; +}); + describe("readAuthConfig", () => { const originalEnv = { ...process.env }; @@ -48,6 +59,52 @@ describe("readAuthConfig", () => { }); }); +describe("apiGet", () => { + it("should pass GET params in the tRPC { json: ... } input envelope", async () => { + process.env.DOKPLOY_URL = "https://test.dokploy.com"; + process.env.DOKPLOY_API_KEY = "test-key-123"; + + const urls: string[] = []; + const axiosMock = await import("axios"); + axiosMock.default.create = () => + ({ + get: async (url: string) => { + urls.push(url); + return { data: {} }; + }, + }) as never; + + const { apiGet } = await import("../src/client.js"); + await apiGet("compose.one", { composeId: "abc123", limit: 1 }); + + expect(urls[0]).toBe( + `/trpc/compose.one?input=${encodeURIComponent( + JSON.stringify({ json: { composeId: "abc123", limit: 1 } }), + )}`, + ); + }); + + it("should omit the input query string when no params are passed", async () => { + process.env.DOKPLOY_URL = "https://test.dokploy.com"; + process.env.DOKPLOY_API_KEY = "test-key-123"; + + const urls: string[] = []; + const axiosMock = await import("axios"); + axiosMock.default.create = () => + ({ + get: async (url: string) => { + urls.push(url); + return { data: {} }; + }, + }) as never; + + const { apiGet } = await import("../src/client.js"); + await apiGet("project.all"); + + expect(urls[0]).toBe("/trpc/project.all"); + }); +}); + describe("saveAuthConfig", () => { it("should write config with correct structure", async () => { const { saveAuthConfig } = await import("../src/client.js");