Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/js-client-grpc/src/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function createApis(baseUrl: string, {timeout, apiKey, compression, heade
(next) => (req) =>
next(req).catch((error) => {
if (error instanceof ConnectError && error.code === Code.ResourceExhausted) {
const retryAfterHeader = error.metadata.get('retry-after')?.[0];
const retryAfterHeader = error.metadata.get('retry-after');
if (retryAfterHeader) {
throw new QdrantClientResourceExhaustedError(error.rawMessage, retryAfterHeader);
}
Expand Down
8 changes: 7 additions & 1 deletion packages/js-client-grpc/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ export class QdrantClientConfigError extends CustomError {}
export class QdrantClientResourceExhaustedError extends ConnectError {
retry_after: number;

// ConnectError overrides `instanceof` to match any ConnectError, so restore the prototype chain check.
static override [Symbol.hasInstance](value: unknown): boolean {
return Function.prototype[Symbol.hasInstance].call(this, value);
}

constructor(message: string, retryAfter: string) {
// Keep the `ConnectError` name set by the base class: connect only passes an error thrown
// in an interceptor through unchanged if it is recognized as a ConnectError.
super(message, Code.ResourceExhausted);
this.name = this.constructor.name;

const retryAfterNumber = Number(retryAfter);
if (isNaN(retryAfterNumber)) {
Expand Down
61 changes: 61 additions & 0 deletions packages/js-client-grpc/tests/unit/api-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {createServer, Http2Server, ServerHttp2Session} from 'node:http2';
import {AddressInfo} from 'node:net';
import {Code, ConnectError, ConnectRouter} from '@connectrpc/connect';
import {connectNodeAdapter} from '@connectrpc/connect-node';
import {afterEach, describe, expect, test} from 'vitest';
import {createApis} from '../../src/api-client.js';
import {QdrantClientResourceExhaustedError} from '../../src/errors.js';
import {Qdrant} from '../../src/proto/qdrant_pb.js';

describe('apiClient', () => {
let server: Http2Server | undefined;
const sessions = new Set<ServerHttp2Session>();

afterEach(async () => {
const current = server;
server = undefined;
if (current) {
// The client keeps its HTTP/2 session open, so close it to let the server shut down.
sessions.forEach((session) => session.destroy());
sessions.clear();
await new Promise<void>((resolve) => current.close(() => resolve()));
}
});

const startServer = async (error: ConnectError) => {
const routes = (router: ConnectRouter) =>
router.service(Qdrant, {
healthCheck() {
throw error;
},
});
const current = createServer(connectNodeAdapter({routes}));
current.on('session', (session) => sessions.add(session));
server = current;
await new Promise<void>((resolve) => current.listen(0, '127.0.0.1', resolve));
return `http://127.0.0.1:${(current.address() as AddressInfo).port}`;
};

test('resource exhausted with retry-after', async () => {
const baseUrl = await startServer(
new ConnectError('rate limited', Code.ResourceExhausted, {'retry-after': '15'}),
);
const apis = createApis(baseUrl, {timeout: Infinity, compression: false});

const error = await apis.service.healthCheck({}).catch((e: unknown) => e);

expect(error).toBeInstanceOf(QdrantClientResourceExhaustedError);
expect(error).toMatchObject({code: Code.ResourceExhausted, retry_after: 15});
});

test('other errors are not QdrantClientResourceExhaustedError', async () => {
const baseUrl = await startServer(new ConnectError('not found', Code.NotFound));
const apis = createApis(baseUrl, {timeout: Infinity, compression: false});

const error = await apis.service.healthCheck({}).catch((e: unknown) => e);

expect(error).toBeInstanceOf(ConnectError);
expect(error).toMatchObject({code: Code.NotFound});
expect(error).not.toBeInstanceOf(QdrantClientResourceExhaustedError);
});
});
Loading