proxy-forwarder-host-header-fix.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { describe, expect, it } from "vitest";
  2. import type { Provider } from "@/types/provider";
  3. import { ProxyForwarder } from "@/app/v1/_lib/proxy/forwarder";
  4. import { HeaderProcessor } from "@/app/v1/_lib/headers";
  5. import { ProxySession } from "@/app/v1/_lib/proxy/session";
  6. function createSession({
  7. userAgent,
  8. headers,
  9. }: {
  10. userAgent: string | null;
  11. headers: Headers;
  12. }): ProxySession {
  13. const session = Object.create(ProxySession.prototype);
  14. Object.assign(session, {
  15. startTime: Date.now(),
  16. method: "POST",
  17. requestUrl: new URL("https://example.com/v1/messages"),
  18. headers,
  19. originalHeaders: new Headers(headers),
  20. headerLog: JSON.stringify(Object.fromEntries(headers.entries())),
  21. request: { message: {}, log: "" },
  22. userAgent,
  23. context: null,
  24. clientAbortSignal: null,
  25. userName: "test-user",
  26. authState: null,
  27. provider: null,
  28. messageContext: null,
  29. sessionId: null,
  30. requestSequence: 1,
  31. originalFormat: "claude",
  32. providerType: null,
  33. originalModelName: null,
  34. originalUrlPathname: null,
  35. providerChain: [],
  36. cacheTtlResolved: null,
  37. context1mApplied: false,
  38. cachedPriceData: undefined,
  39. cachedBillingModelSource: undefined,
  40. isHeaderModified: (key: string) => {
  41. const original = session.originalHeaders?.get(key);
  42. const current = session.headers.get(key);
  43. return original !== current;
  44. },
  45. });
  46. return session as any;
  47. }
  48. describe("ProxyForwarder - Host header correction for multi-endpoint providers", () => {
  49. it("buildHeaders sets Host from provider.url, which may differ from actual target", () => {
  50. const session = createSession({
  51. userAgent: "Test/1.0",
  52. headers: new Headers([["user-agent", "Test/1.0"]]),
  53. });
  54. const provider = {
  55. providerType: "claude",
  56. url: "https://api.anthropic.com/v1",
  57. key: "test-key",
  58. preserveClientIp: false,
  59. } as unknown as Provider;
  60. const { buildHeaders } = ProxyForwarder as unknown as {
  61. buildHeaders: (session: ProxySession, provider: Provider) => Headers;
  62. };
  63. const resultHeaders = buildHeaders(session, provider);
  64. // buildHeaders uses provider.url for Host
  65. expect(resultHeaders.get("host")).toBe("api.anthropic.com");
  66. });
  67. it("Host header must be corrected when activeEndpoint baseUrl differs from provider.url", () => {
  68. const session = createSession({
  69. userAgent: "Test/1.0",
  70. headers: new Headers([["user-agent", "Test/1.0"]]),
  71. });
  72. const provider = {
  73. providerType: "claude",
  74. url: "https://api.anthropic.com/v1",
  75. key: "test-key",
  76. preserveClientIp: false,
  77. } as unknown as Provider;
  78. const { buildHeaders } = ProxyForwarder as unknown as {
  79. buildHeaders: (session: ProxySession, provider: Provider) => Headers;
  80. };
  81. const processedHeaders = buildHeaders(session, provider);
  82. // Initial Host from provider.url
  83. expect(processedHeaders.get("host")).toBe("api.anthropic.com");
  84. // Simulate: activeEndpoint has a different baseUrl (e.g. regional endpoint)
  85. const proxyUrl = "https://eu-west.anthropic.com/v1/messages";
  86. const actualHost = HeaderProcessor.extractHost(proxyUrl);
  87. processedHeaders.set("host", actualHost);
  88. // After correction, Host matches actual target
  89. expect(processedHeaders.get("host")).toBe("eu-west.anthropic.com");
  90. });
  91. it("Host header must be corrected when MCP passthrough URL differs from provider.url", () => {
  92. const session = createSession({
  93. userAgent: "Test/1.0",
  94. headers: new Headers([["user-agent", "Test/1.0"]]),
  95. });
  96. const provider = {
  97. providerType: "claude",
  98. url: "https://api.minimaxi.com/anthropic",
  99. key: "test-key",
  100. preserveClientIp: false,
  101. } as unknown as Provider;
  102. const { buildHeaders } = ProxyForwarder as unknown as {
  103. buildHeaders: (session: ProxySession, provider: Provider) => Headers;
  104. };
  105. const processedHeaders = buildHeaders(session, provider);
  106. // Initial Host from provider.url (includes /anthropic path)
  107. expect(processedHeaders.get("host")).toBe("api.minimaxi.com");
  108. // MCP passthrough: base domain extraction strips path, URL stays same host
  109. // But if mcpPassthroughUrl points to a different host:
  110. const mcpProxyUrl = "https://mcp.minimaxi.com/v1/tools/list";
  111. const actualHost = HeaderProcessor.extractHost(mcpProxyUrl);
  112. processedHeaders.set("host", actualHost);
  113. expect(processedHeaders.get("host")).toBe("mcp.minimaxi.com");
  114. });
  115. it("Host header remains correct when provider.url and proxyUrl share the same host", () => {
  116. const session = createSession({
  117. userAgent: "Test/1.0",
  118. headers: new Headers([["user-agent", "Test/1.0"]]),
  119. });
  120. const provider = {
  121. providerType: "claude",
  122. url: "https://api.anthropic.com/v1",
  123. key: "test-key",
  124. preserveClientIp: false,
  125. } as unknown as Provider;
  126. const { buildHeaders } = ProxyForwarder as unknown as {
  127. buildHeaders: (session: ProxySession, provider: Provider) => Headers;
  128. };
  129. const processedHeaders = buildHeaders(session, provider);
  130. // Same host, correction is a no-op
  131. const proxyUrl = "https://api.anthropic.com/v1/messages";
  132. const actualHost = HeaderProcessor.extractHost(proxyUrl);
  133. processedHeaders.set("host", actualHost);
  134. expect(processedHeaders.get("host")).toBe("api.anthropic.com");
  135. });
  136. it("Host header handles port numbers correctly", () => {
  137. const proxyUrl = "https://api.example.com:8443/v1/messages";
  138. const host = HeaderProcessor.extractHost(proxyUrl);
  139. expect(host).toBe("api.example.com:8443");
  140. });
  141. });