Fix MCP auth timeouts and return retryable 504 on auth stalls.

Add shared FETCH_TIMEOUT_MS, timeout both API-key and OAuth auth paths, disable SDK retries for a true 30s bound, and distinguish auth timeouts from invalid credentials so clients get 504 instead of 401.
This commit is contained in:
Mahesh Sanikommu 2026-06-21 23:04:07 -07:00
parent 3d5579a006
commit 790f989d2f
4 changed files with 81 additions and 36 deletions

View file

@ -4,6 +4,8 @@
* This validates OAuth tokens and API keys by calling the main Supermemory API,
*/
import { FETCH_TIMEOUT_MS } from "./constants"
export interface AuthUser {
userId: string
apiKey: string
@ -11,6 +13,18 @@ export interface AuthUser {
name?: string
}
export type AuthValidationResult =
| { status: "success"; user: AuthUser }
| { status: "invalid" }
| { status: "timeout" }
function isFetchTimeout(error: unknown): boolean {
return (
error instanceof Error &&
(error.name === "AbortError" || error.name === "TimeoutError")
)
}
/**
* Check if a token is an API key (starts with "sm_")
*/
@ -25,13 +39,14 @@ export function isApiKey(token: string): boolean {
export async function validateApiKey(
apiKey: string,
apiUrl: string,
): Promise<AuthUser | null> {
): Promise<AuthValidationResult> {
try {
const sessionResponse = await fetch(`${apiUrl}/v3/session`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
},
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
})
if (!sessionResponse.ok) {
@ -56,7 +71,7 @@ export async function validateApiKey(
} else {
console.error("API key validation failed:", status, responseText)
}
return null
return { status: "invalid" }
}
const sessionData = (await sessionResponse.json()) as {
@ -72,20 +87,27 @@ export async function validateApiKey(
if (!sessionData?.user?.id) {
console.error("Missing user.id in session response:", sessionData)
return null
return { status: "invalid" }
}
console.log("API key validated for user:", sessionData.user.id)
return {
userId: sessionData.user.id,
apiKey: apiKey,
email: sessionData.user.email,
name: sessionData.user.name,
status: "success",
user: {
userId: sessionData.user.id,
apiKey: apiKey,
email: sessionData.user.email,
name: sessionData.user.name,
},
}
} catch (error) {
if (isFetchTimeout(error)) {
console.error("API key validation timed out")
return { status: "timeout" }
}
console.error("API key validation error:", error)
return null
return { status: "invalid" }
}
}
@ -96,14 +118,14 @@ export async function validateApiKey(
export async function validateOAuthToken(
token: string,
apiUrl: string,
): Promise<AuthUser | null> {
): Promise<AuthValidationResult> {
try {
const sessionResponse = await fetch(`${apiUrl}/v3/mcp/session-with-key`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
signal: AbortSignal.timeout(30_000),
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
})
if (!sessionResponse.ok) {
@ -128,7 +150,7 @@ export async function validateOAuthToken(
} else {
console.error("Token validation failed:", status, responseText)
}
return null
return { status: "invalid" }
}
const sessionData = (await sessionResponse.json()) as {
@ -144,19 +166,26 @@ export async function validateOAuthToken(
"Missing userId or apiKey in session response:",
sessionData,
)
return null
return { status: "invalid" }
}
console.log("OAuth validated, got API key for user:", sessionData.userId)
return {
userId: sessionData.userId,
apiKey: sessionData.apiKey,
email: sessionData.email,
name: sessionData.name,
status: "success",
user: {
userId: sessionData.userId,
apiKey: sessionData.apiKey,
email: sessionData.email,
name: sessionData.name,
},
}
} catch (error) {
if (isFetchTimeout(error)) {
console.error("Token validation timed out")
return { status: "timeout" }
}
console.error("Token validation error:", error)
return null
return { status: "invalid" }
}
}

View file

@ -1,8 +1,9 @@
import Supermemory from "supermemory"
import Supermemory, { APIConnectionTimeoutError } from "supermemory"
import { FETCH_TIMEOUT_MS } from "./constants"
const MAX_CHARS = 200000 // ~50k tokens (character-based limit)
const DEFAULT_PROJECT_ID = "sm_project_default"
const FETCH_TIMEOUT_MS = 30_000
interface MemoryRichFields {
metadata?: Record<string, unknown> | null
@ -144,6 +145,7 @@ export class SupermemoryClient {
apiKey: bearerToken,
baseURL: apiUrl,
timeout: FETCH_TIMEOUT_MS,
maxRetries: 0,
})
this.containerTag = containerTag || DEFAULT_PROJECT_ID
}
@ -394,10 +396,10 @@ export class SupermemoryClient {
}
private handleError(error: unknown): never {
// Handle request timeout (AbortSignal.timeout or explicit abort)
if (
error instanceof Error &&
(error.name === "AbortError" || error.name === "TimeoutError")
error instanceof APIConnectionTimeoutError ||
(error instanceof Error &&
(error.name === "AbortError" || error.name === "TimeoutError"))
) {
throw new Error("Request to Supermemory API timed out")
}

View file

@ -0,0 +1 @@
export const FETCH_TIMEOUT_MS = 30_000

View file

@ -2,6 +2,7 @@ import { cors } from "hono/cors"
import { Hono, type Context } from "hono"
import { SupermemoryMCP } from "./server"
import { isApiKey, validateApiKey, validateOAuthToken } from "./auth"
import { FETCH_TIMEOUT_MS } from "./constants"
import { initPosthog } from "./posthog"
import type { ContentfulStatusCode } from "hono/utils/http-status"
@ -89,7 +90,7 @@ app.get("/.well-known/oauth-authorization-server", async (c) => {
// Fetch the authorization server metadata from the main API
const response = await fetch(
`${apiUrl}/.well-known/oauth-authorization-server`,
{ signal: AbortSignal.timeout(30_000) },
{ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) },
)
if (!response.ok) {
@ -136,22 +137,32 @@ const handleMcpRequest = async (c: Context<{ Bindings: Bindings }>) => {
})
}
let authUser: {
userId: string
apiKey: string
email?: string
name?: string
} | null = null
let authResult = isApiKey(token)
? await validateApiKey(token, apiUrl)
: await validateOAuthToken(token, apiUrl)
if (isApiKey(token)) {
console.log("Authenticating with API key")
authUser = await validateApiKey(token, apiUrl)
} else {
console.log("Authenticating with OAuth token")
authUser = await validateOAuthToken(token, apiUrl)
if (authResult.status === "timeout") {
return new Response(
JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Authentication service timed out. Please try again.",
},
id: null,
}),
{
status: 504,
headers: {
"Content-Type": "application/json",
"Access-Control-Expose-Headers": "WWW-Authenticate",
"Access-Control-Allow-Origin": "*",
},
},
)
}
if (!authUser) {
if (authResult.status !== "success") {
const errorMessage = isApiKey(token)
? "Unauthorized: Invalid or expired API key"
: "Unauthorized: Invalid or expired token"
@ -177,6 +188,8 @@ const handleMcpRequest = async (c: Context<{ Bindings: Bindings }>) => {
)
}
const authUser = authResult.user
// Create execution context with authenticated user props
const ctx = {
...c.executionCtx,