HTTP Subscription Link
On this page
httpSubscriptionLink is a terminating link that uses Server-sent Events (SSE) for subscriptions.
SSE is a good option for real-time as it's a bit easier than setting up a WebSockets-server.
Setup
To use httpSubscriptionLink, you need to use a splitLink to make it explicit that we want to use SSE for subscriptions.
import {
function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient,
function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink,
function httpSubscriptionLink<TInferrable extends InferrableClientTypes, TEventSource extends EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<inferClientTypes<TInferrable>, TEventSource>): TRPCLink<TInferrable>httpSubscriptionLink,
function loggerLink<TRouter extends AnyRouter = AnyRouter>(opts?: LoggerLinkOptions<TRouter>): TRPCLink<TRouter>loggerLink,
function splitLink<TRouter extends AnyRouter = AnyRouter>(opts: {
condition: (op: Operation) => boolean;
true: TRPCLink<TRouter> | TRPCLink<TRouter>[];
false: TRPCLink<TRouter> | TRPCLink<TRouter>[];
}): TRPCLink<TRouter>
splitLink,
} from '@trpc/client';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
const const trpcClient: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpcClient = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
/**
* @see https://trpc.io/docs/v11/client/links
*/
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
// adds pretty logs to your console in development and logs errors in production
loggerLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts?: LoggerLinkOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | undefined): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
loggerLink(),
splitLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: {
condition: (op: Operation) => boolean;
true: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>[];
false: TRPCLink<...> | TRPCLink<...>[];
}): TRPCLink<...>
splitLink({
// uses the httpSubscriptionLink for subscriptions
condition: (op: Operation) => booleancondition: (op: Operationop) => op: Operationop.type: "query" | "mutation" | "subscription"type === 'subscription',
true: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
true: httpSubscriptionLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<PickTypes<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>, EventSourceLike.AnyConstructor>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
httpSubscriptionLink({
UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: `/api/trpc`,
}),
false: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
false: httpBatchLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink({
url: string | URLurl: `/api/trpc`,
}),
}),
],
});Headers and authorization / authentication
Web apps
Same domain
If you're doing a web application, cookies are sent as part of the request as long as your client is on the same domain as the server.
Cross-domain
If the client and server are not on the same domain, you can use withCredentials: true (read more on MDN here).
Example:
// [...]
httpSubscriptionLink<InferrableClientTypes, EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<PickTypes<AnyClientTypes>, EventSourceLike.AnyConstructor>): TRPCLink<InferrableClientTypes>httpSubscriptionLink({
UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: 'https://example.com/api/trpc',
eventSourceOptions?: anyeventSourceOptions() {
return {
withCredentials: booleanwithCredentials: true, // <---
};
},
});Custom headers through ponyfill
Recommended for non-web environments
You can ponyfill EventSource and use the eventSourceOptions -callback to populate headers.
import {
function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient,
function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink,
function httpSubscriptionLink<TInferrable extends InferrableClientTypes, TEventSource extends EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<inferClientTypes<TInferrable>, TEventSource>): TRPCLink<TInferrable>httpSubscriptionLink,
function splitLink<TRouter extends AnyRouter = AnyRouter>(opts: {
condition: (op: Operation) => boolean;
true: TRPCLink<TRouter> | TRPCLink<TRouter>[];
false: TRPCLink<TRouter> | TRPCLink<TRouter>[];
}): TRPCLink<TRouter>
splitLink,
} from '@trpc/client';
import { class EventSourcePolyfillEventSourcePolyfill } from 'event-source-polyfill';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
// Initialize the tRPC client
const const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpc = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
splitLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: {
condition: (op: Operation) => boolean;
true: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>[];
false: TRPCLink<...> | TRPCLink<...>[];
}): TRPCLink<...>
splitLink({
condition: (op: Operation) => booleancondition: (op: Operationop) => op: Operationop.type: "query" | "mutation" | "subscription"type === 'subscription',
true: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
true: httpSubscriptionLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, typeof EventSourcePolyfill>(opts: HTTPSubscriptionLinkOptions<PickTypes<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>, typeof EventSourcePolyfill>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
httpSubscriptionLink({
UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: 'http://localhost:3000',
// ponyfill EventSource
EventSource?: typeof EventSourcePolyfill | undefinedEventSource: class EventSourcePolyfillEventSourcePolyfill,
// options to pass to the EventSourcePolyfill constructor
eventSourceOptions?: EventSourcePolyfillInit | ((opts: {
op: Operation;
}) => EventSourcePolyfillInit | Promise<EventSourcePolyfillInit | undefined> | undefined) | undefined
eventSourceOptions: async ({ op: Operationop }) => {
// ^ Includes the operation that's being executed
// you can use this to generate a signature for the operation
const const signature: stringsignature = await function getSignature(op: any): Promise<string>getSignature(op: Operationop);
return {
EventSourcePolyfillInit.headers?: {
[name: string]: string;
} | undefined
headers: {
authorization: stringauthorization: 'Bearer supersecret',
'x-signature': const signature: stringsignature,
},
};
},
}),
false: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
false: httpBatchLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink({
url: string | URLurl: 'http://localhost:3000',
}),
}),
],
});Updating configuration on an active connection
httpSubscriptionLink leverages SSE through EventSource, ensuring that connections encountering errors like network failures or bad response codes are automatically retried. However, EventSource does not allow re-execution of the eventSourceOptions() or url() options to update its configuration, which is particularly important in scenarios where authentication has expired since the last connection.
To address this limitation, you can use a retryLink in conjunction with httpSubscriptionLink. This approach ensures that the connection is re-established with the latest configuration, including any updated authentication details.
import {
function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient,
function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink,
function httpSubscriptionLink<TInferrable extends InferrableClientTypes, TEventSource extends EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<inferClientTypes<TInferrable>, TEventSource>): TRPCLink<TInferrable>httpSubscriptionLink,
function retryLink<TInferrable extends InferrableClientTypes>(opts: RetryLinkOptions<TInferrable>): TRPCLink<TInferrable>retryLink,
function splitLink<TRouter extends AnyRouter = AnyRouter>(opts: {
condition: (op: Operation) => boolean;
true: TRPCLink<TRouter> | TRPCLink<TRouter>[];
false: TRPCLink<TRouter> | TRPCLink<TRouter>[];
}): TRPCLink<TRouter>
splitLink,
} from '@trpc/client';
import {
class EventSourcePolyfillEventSourcePolyfill,
EventSourcePolyfillInit,
} from 'event-source-polyfill';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
// Initialize the tRPC client
const const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpc = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
splitLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: {
condition: (op: Operation) => boolean;
true: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>[];
false: TRPCLink<...> | TRPCLink<...>[];
}): TRPCLink<...>
splitLink({
condition: (op: Operation) => booleancondition: (op: Operationop) => op: Operationop.type: "query" | "mutation" | "subscription"type === 'subscription',
false: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
false: httpBatchLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink({
url: string | URLurl: 'http://localhost:3000',
}),
true: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
true: [
retryLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: RetryLinkOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
retryLink({
RetryLinkOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>>.retry: (opts: RetryFnOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>) => boolean
retry: (opts: RetryFnOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
opts) => {
opts: RetryFnOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
opts.RetryFnOptions<TInferrable extends InferrableClientTypes>.op: Operationop.type; const const code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST" | undefinedcode = opts: RetryFnOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
opts.RetryFnOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>>.error: TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
error.TRPCClientError<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>>.data: Maybe<DefaultErrorData>data?.code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST" | undefinedcode;
if (!const code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST" | undefinedcode) {
// This shouldn't happen as our httpSubscriptionLink will automatically retry within when there's a non-parsable response
var console: Consoleconsole.Console.error(...data: any[]): voiderror('No error code found, retrying', opts: RetryFnOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
opts);
return true;
}
if (const code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST"code === 'UNAUTHORIZED' || const code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST"code === 'FORBIDDEN') {
var console: Consoleconsole.Console.log(...data: any[]): voidlog('Retrying due to 401/403 error');
return true;
}
return false;
},
}),
httpSubscriptionLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, typeof EventSourcePolyfill>(opts: HTTPSubscriptionLinkOptions<PickTypes<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>, typeof EventSourcePolyfill>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
httpSubscriptionLink({
UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: async () => {
// calculate the latest URL if needed...
return function getAuthenticatedUri(): stringgetAuthenticatedUri();
},
// ponyfill EventSource
EventSource?: typeof EventSourcePolyfill | undefinedEventSource: class EventSourcePolyfillEventSourcePolyfill,
eventSourceOptions?: EventSourcePolyfillInit | ((opts: {
op: Operation;
}) => EventSourcePolyfillInit | Promise<EventSourcePolyfillInit | undefined> | undefined) | undefined
eventSourceOptions: async () => {
// ...or maybe renew an access token
const const token: stringtoken = await const auth: {
getOrRenewToken(): Promise<string>;
}
auth.function getOrRenewToken(): Promise<string>getOrRenewToken();
return {
EventSourcePolyfillInit.headers?: {
[name: string]: string;
} | undefined
headers: {
authorization: stringauthorization: `Bearer ${const token: stringtoken}`,
},
};
},
}),
],
}),
],
});Connection params
In order to authenticate with EventSource, you can define connectionParams in httpSubscriptionLink. This will be sent as part of the URL, which is why other methods are preferred.
import type { type CreateHTTPContextOptions = {
req: http.IncomingMessage;
res: http.ServerResponse;
info: TRPCRequestInfo;
}
CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
export const const createContext: (opts: CreateHTTPContextOptions) => Promise<{}>createContext = async (opts: CreateHTTPContextOptionsopts: type CreateHTTPContextOptions = {
req: http.IncomingMessage;
res: http.ServerResponse;
info: TRPCRequestInfo;
}
CreateHTTPContextOptions) => {
const token = opts: CreateHTTPContextOptionsopts.info: TRPCRequestInfoinfo.TRPCRequestInfo.connectionParams: Dict<string> | nullconnectionParams?.string | undefinedtoken;
// [... authenticate]
return {};
};
export type type Context = {}Context = type Awaited<T> = T extends null | undefined ? T : T extends object & {
then(onfulfilled: infer F, ...args: infer _): any;
} ? F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never : T
Awaited<type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyReturnType<typeof const createContext: (opts: CreateHTTPContextOptions) => Promise<{}>createContext>>;import {
function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient,
function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink,
function httpSubscriptionLink<TInferrable extends InferrableClientTypes, TEventSource extends EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<inferClientTypes<TInferrable>, TEventSource>): TRPCLink<TInferrable>httpSubscriptionLink,
function splitLink<TRouter extends AnyRouter = AnyRouter>(opts: {
condition: (op: Operation) => boolean;
true: TRPCLink<TRouter> | TRPCLink<TRouter>[];
false: TRPCLink<TRouter> | TRPCLink<TRouter>[];
}): TRPCLink<TRouter>
splitLink,
} from '@trpc/client';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
// Initialize the tRPC client
const const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpc = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
splitLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: {
condition: (op: Operation) => boolean;
true: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>[];
false: TRPCLink<...> | TRPCLink<...>[];
}): TRPCLink<...>
splitLink({
condition: (op: Operation) => booleancondition: (op: Operationop) => op: Operationop.type: "query" | "mutation" | "subscription"type === 'subscription',
true: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
true: httpSubscriptionLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<PickTypes<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>, EventSourceLike.AnyConstructor>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
httpSubscriptionLink({
UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: 'http://localhost:3000',
UrlOptionsWithConnectionParams.connectionParams?: CallbackOrValue<Dict<string> | null> | undefinedconnectionParams: async () => {
// Will be serialized as part of the URL
return {
token: stringtoken: 'supersecret',
};
},
}),
false: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
false: httpBatchLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink({
url: string | URLurl: 'http://localhost:3000',
}),
}),
],
});Timeout Configuration
The httpSubscriptionLink supports configuring a timeout for inactivity through the reconnectAfterInactivityMs option. If no messages (including ping messages) are received within the specified timeout period, the connection will be marked as "connecting" and automatically attempt to reconnect.
The timeout configuration is set on the server side when initializing tRPC:
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
export const const t: TRPCRootObject<object, object, {
sse: {
client: {
reconnectAfterInactivityMs: number;
};
};
}, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t = const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.create<{
sse: {
client: {
reconnectAfterInactivityMs: number;
};
};
}>(opts?: {
sse: {
client: {
reconnectAfterInactivityMs: number;
};
};
} | undefined): TRPCRootObject<object, object, {
sse: {
client: {
reconnectAfterInactivityMs: number;
};
};
}, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
create({
sse: {
client: {
reconnectAfterInactivityMs: number;
};
}
sse: {
client: {
reconnectAfterInactivityMs: number;
}
client: {
reconnectAfterInactivityMs: numberreconnectAfterInactivityMs: 3_000,
},
},
});Server Ping Configuration
The server can be configured to send periodic ping messages to keep the connection alive and prevent timeout disconnections. This is particularly useful when combined with the reconnectAfterInactivityMs-option.
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
export const const t: TRPCRootObject<object, object, {
sse: {
ping: {
enabled: true;
intervalMs: number;
};
};
}, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t = const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.create<{
sse: {
ping: {
enabled: true;
intervalMs: number;
};
};
}>(opts?: {
sse: {
ping: {
enabled: true;
intervalMs: number;
};
};
} | undefined): TRPCRootObject<object, object, {
sse: {
ping: {
enabled: true;
intervalMs: number;
};
};
}, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
create({
sse: {
ping: {
enabled: true;
intervalMs: number;
};
}
sse: {
// Maximum duration of a single SSE connection in milliseconds
// maxDurationMs: 60_000,
ping: {
enabled: true;
intervalMs: number;
}
ping: {
// Enable periodic ping messages to keep connection alive
enabled: trueenabled: true,
// Send ping message every 2s
intervalMs: numberintervalMs: 2_000,
},
// client: {
// reconnectAfterInactivityMs: 3_000
// }
},
});Compatibility (React Native)
The httpSubscriptionLink makes use of the EventSource API, Streams API, and AsyncIterators, these are not natively supported by React Native and will have to be ponyfilled.
To ponyfill EventSource we recommend to use a polyfill that utilizes the networking library exposed by React Native, over using a polyfill that uses the XMLHttpRequest API. Libraries that polyfill EventSource using XMLHttpRequest fail to reconnect after the app has been in the background. Consider using the rn-eventsource-reborn package.
The Streams API can be ponyfilled using the web-streams-polyfill package.
AsyncIterators can be polyfilled using the @azure/core-asynciterator-polyfill package.
Installation
Install the required polyfills:
pnpm add rn-eventsource-reborn web-streams-polyfill @azure/core-asynciterator-polyfillnpm install rn-eventsource-reborn web-streams-polyfill @azure/core-asynciterator-polyfillyarn add rn-eventsource-reborn web-streams-polyfill @azure/core-asynciterator-polyfillbun add rn-eventsource-reborn web-streams-polyfill @azure/core-asynciterator-polyfillAdd the polyfills to your project before the link is used (e.g. where you add your TRPCReact.Provider):
import '@azure/core-asynciterator-polyfill';
import { RNEventSource } from 'rn-eventsource-reborn';
import { ReadableStream, TransformStream } from 'web-streams-polyfill';
globalThis.ReadableStream = globalThis.ReadableStream || ReadableStream;
globalThis.TransformStream = globalThis.TransformStream || TransformStream;Once the ponyfills are added, you can continue setting up the httpSubscriptionLink as described in the setup section.
httpSubscriptionLink Options
type type HTTPSubscriptionLinkOptions<TRoot extends unknown, TEventSource extends unknown = { new (url: string | URL, eventSourceInitDict?: EventSourceInit): EventSource; prototype: EventSource; readonly CONNECTING: 0; readonly OPEN: 1; readonly CLOSED: 2; }> = {
url: string | (() => string | Promise<string>);
connectionParams?: Record<string, string> | null | (() => Record<string, string> | null | Promise<Record<string, string> | null>);
transformer?: DataTransformerOptions;
EventSource?: TEventSource;
eventSourceOptions?: EventSourceLike.InitDictOf<TEventSource> | ((opts: {
op: Operation;
}) => EventSourceLike.InitDictOf<TEventSource> | Promise<EventSourceLike.InitDictOf<TEventSource>>);
}
HTTPSubscriptionLinkOptions<
function (type parameter) TRoot in type HTTPSubscriptionLinkOptions<TRoot extends unknown, TEventSource extends unknown = { new (url: string | URL, eventSourceInitDict?: EventSourceInit): EventSource; prototype: EventSource; readonly CONNECTING: 0; readonly OPEN: 1; readonly CLOSED: 2; }>TRoot extends type AnyClientTypes = anyAnyClientTypes,
function (type parameter) TEventSource in type HTTPSubscriptionLinkOptions<TRoot extends unknown, TEventSource extends unknown = { new (url: string | URL, eventSourceInitDict?: EventSourceInit): EventSource; prototype: EventSource; readonly CONNECTING: 0; readonly OPEN: 1; readonly CLOSED: 2; }>TEventSource extends EventSourceLike.type EventSourceLike.AnyConstructor = anyAnyConstructor = typeof var EventSource: {
new (url: string | URL, eventSourceInitDict?: EventSourceInit): EventSource;
prototype: EventSource;
readonly CONNECTING: 0;
readonly OPEN: 1;
readonly CLOSED: 2;
}
EventSource,
> = {
/**
* The URL to connect to (can be a function that returns a URL)
*/
url: string | (() => string | Promise<string>)url: string | (() => string | interface Promise<T>Promise<string>);
/**
* Connection params that are available in `createContext()`
* Serialized as part of the URL under the `connectionParams` query parameter
*/
connectionParams?: Record<string, string> | (() => Record<string, string> | null | Promise<Record<string, string> | null>) | null | undefinedconnectionParams?:
| type Record<K extends keyof any, T> = { [P in K]: T; }Record<string, string>
| null
| (() =>
| type Record<K extends keyof any, T> = { [P in K]: T; }Record<string, string>
| null
| interface Promise<T>Promise<type Record<K extends keyof any, T> = { [P in K]: T; }Record<string, string> | null>);
/**
* Data transformer
* @see https://trpc.io/docs/v11/data-transformers
*/
transformer?: anytransformer?: type DataTransformerOptions = anyDataTransformerOptions;
/**
* EventSource ponyfill
*/
EventSource?: TEventSource | undefinedEventSource?: function (type parameter) TEventSource in type HTTPSubscriptionLinkOptions<TRoot extends unknown, TEventSource extends unknown = { new (url: string | URL, eventSourceInitDict?: EventSourceInit): EventSource; prototype: EventSource; readonly CONNECTING: 0; readonly OPEN: 1; readonly CLOSED: 2; }>TEventSource;
/**
* EventSource options or a callback that returns them
*/
eventSourceOptions?: anyeventSourceOptions?:
| EventSourceLike.type EventSourceLike.InitDictOf<T> = anyInitDictOf<function (type parameter) TEventSource in type HTTPSubscriptionLinkOptions<TRoot extends unknown, TEventSource extends unknown = { new (url: string | URL, eventSourceInitDict?: EventSourceInit): EventSource; prototype: EventSource; readonly CONNECTING: 0; readonly OPEN: 1; readonly CLOSED: 2; }>TEventSource>
| ((opts: {
op: Operation;
}
opts: {
op: anyop: type Operation = anyOperation;
}) =>
| EventSourceLike.type EventSourceLike.InitDictOf<T> = anyInitDictOf<function (type parameter) TEventSource in type HTTPSubscriptionLinkOptions<TRoot extends unknown, TEventSource extends unknown = { new (url: string | URL, eventSourceInitDict?: EventSourceInit): EventSource; prototype: EventSource; readonly CONNECTING: 0; readonly OPEN: 1; readonly CLOSED: 2; }>TEventSource>
| interface Promise<T>Promise<EventSourceLike.type EventSourceLike.InitDictOf<T> = anyInitDictOf<function (type parameter) TEventSource in type HTTPSubscriptionLinkOptions<TRoot extends unknown, TEventSource extends unknown = { new (url: string | URL, eventSourceInitDict?: EventSourceInit): EventSource; prototype: EventSource; readonly CONNECTING: 0; readonly OPEN: 1; readonly CLOSED: 2; }>TEventSource>>);
};SSE Options on the server
export interface interface SSEStreamProducerOptions<TValue = unknown>SSEStreamProducerOptions<function (type parameter) TValue in SSEStreamProducerOptions<TValue = unknown>TValue = unknown> {
SSEStreamProducerOptions<TValue = unknown>.ping?: {
enabled: boolean;
intervalMs?: number;
} | undefined
ping?: {
/**
* Enable ping comments sent from the server
* @default false
*/
enabled: booleanenabled: boolean;
/**
* Interval in milliseconds
* @default 1000
*/
intervalMs?: number | undefinedintervalMs?: number;
};
/**
* Maximum duration in milliseconds for the request before ending the stream
* @default undefined
*/
SSEStreamProducerOptions<TValue = unknown>.maxDurationMs?: number | undefinedmaxDurationMs?: number;
/**
* End the request immediately after data is sent
* Only useful for serverless runtimes that do not support streaming responses
* @default false
*/
SSEStreamProducerOptions<TValue = unknown>.emitAndEndImmediately?: boolean | undefinedemitAndEndImmediately?: boolean;
/**
* Client-specific options - these will be sent to the client as part of the first message
* @default {}
*/
SSEStreamProducerOptions<TValue = unknown>.client?: {
reconnectAfterInactivityMs?: number;
} | undefined
client?: {
/**
* Timeout and reconnect after inactivity in milliseconds
* @default undefined
*/
reconnectAfterInactivityMs?: number | undefinedreconnectAfterInactivityMs?: number;
};
}