Skip to content
Closed
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
96 changes: 93 additions & 3 deletions src/__tests__/linkup-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@ import { z } from 'zod';
import {
LinkupAuthenticationError,
LinkupBudgetLimitExceededError,
LinkupError,
LinkupFetchError,
LinkupFetchResponseTooLargeError,
LinkupFetchUnsupportedContentTypeError,
LinkupInsufficientCreditError,
LinkupInvalidRequestError,
LinkupIpNotWhitelistedError,
LinkupNoResultError,
LinkupPaymentPayloadInvalidError,
LinkupPaymentRequiredError,
LinkupPaymentSettlementFailedError,
LinkupPaymentVerificationFailedError,
LinkupTaskNotFoundError,
LinkupTasksQueueLimitExceededError,
LinkupTooManyRequestsError,
LinkupUnknownError,
LinkupUnsupportedTaskTypeError,
} from '../errors';
import { LinkupClient } from '../linkup-client';
import type { SearchParams, Source } from '../types';
Expand Down Expand Up @@ -819,6 +825,45 @@ describe('LinkupClient', () => {
statusCode: 402,
},
},
{
description: '402 X402_PAYMENT_PAYLOAD_INVALID',
ErrorClass: LinkupPaymentPayloadInvalidError,
expectedMessage: 'Invalid payment payload',
input: {
error: {
code: 'X402_PAYMENT_PAYLOAD_INVALID',
details: [],
message: 'Invalid payment payload',
},
statusCode: 402,
},
},
{
description: '402 X402_PAYMENT_VERIFICATION_FAILED',
ErrorClass: LinkupPaymentVerificationFailedError,
expectedMessage: 'Payment verification failed',
input: {
error: {
code: 'X402_PAYMENT_VERIFICATION_FAILED',
details: [],
message: 'Payment verification failed',
},
statusCode: 402,
},
},
{
description: '402 X402_PAYMENT_SETTLEMENT_FAILED',
ErrorClass: LinkupPaymentSettlementFailedError,
expectedMessage: 'Payment settlement failed',
input: {
error: {
code: 'X402_PAYMENT_SETTLEMENT_FAILED',
details: [],
message: 'Payment settlement failed',
},
statusCode: 402,
},
},
{
description: '401',
ErrorClass: LinkupAuthenticationError,
Expand All @@ -837,11 +882,23 @@ describe('LinkupClient', () => {
statusCode: 403,
},
},
{
description: '403 IP_NOT_WHITELISTED',
ErrorClass: LinkupIpNotWhitelistedError,
expectedMessage: 'IP address 203.0.113.10 is not whitelisted',
input: {
error: {
code: 'IP_NOT_WHITELISTED',
details: [],
message: 'IP address 203.0.113.10 is not whitelisted',
},
statusCode: 403,
},
},
{
description: '403 TASK_TYPE_NOT_SUPPORTED',
ErrorClass: LinkupUnknownError,
expectedMessage:
'Unsupported task type: Extract tasks are not enabled for this organization.',
ErrorClass: LinkupUnsupportedTaskTypeError,
expectedMessage: 'Extract tasks are not enabled for this organization.',
input: {
error: {
code: 'TASK_TYPE_NOT_SUPPORTED',
Expand Down Expand Up @@ -946,6 +1003,39 @@ describe('LinkupClient', () => {
expect(error).toBeInstanceOf(LinkupUnknownError);
expect(error.message).toContain('An unknown error occurred');
});

it.each([
{
input: {
error: { code: 'UNAUTHORIZED', details: [], message: 'Unauthorized action' },
statusCode: 401,
},
parentClass: LinkupAuthenticationError,
},
{
input: {
error: {
code: 'IP_NOT_WHITELISTED',
details: [],
message: 'IP address 203.0.113.10 is not whitelisted',
},
statusCode: 403,
},
parentClass: LinkupAuthenticationError,
},
{
input: {
error: { code: 'X402_PAYMENT_VERIFICATION_FAILED', details: [], message: 'failed' },
statusCode: 402,
},
parentClass: LinkupPaymentRequiredError,
},
])('should keep refined errors in the shared SDK hierarchy', ({ input, parentClass }) => {
const error = refineError(input);

expect(error).toBeInstanceOf(parentClass);
expect(error).toBeInstanceOf(LinkupError);
});
});

describe('x402 mode', () => {
Expand Down
64 changes: 62 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ export class LinkupNoResultError extends LinkupError {
}
}

// Authentication error, raised when the Linkup API returns a 403 status code.
// Authentication error, raised when the Linkup API returns a 401 or 403 status code.
// It is returned when there is an authentication issue, typically when the API key is not valid.
export class LinkupAuthenticationError extends Error {
export class LinkupAuthenticationError extends LinkupError {
constructor(message?: string) {
super(message);
this.name = LinkupAuthenticationError.name;
Expand All @@ -46,6 +46,18 @@ export class LinkupAuthenticationError extends Error {
}
}

// IP whitelist error, raised when the Linkup API returns a 403 status code for a non-whitelisted IP.
export class LinkupIpNotWhitelistedError extends LinkupAuthenticationError {
constructor(message?: string) {
super(message);
this.name = LinkupIpNotWhitelistedError.name;

if ('captureStackTrace' in Error) {
Error.captureStackTrace(this, LinkupIpNotWhitelistedError);
}
}
}

// Task not found error, raised when the Linkup API returns a 404 status code.
// It is returned when a task or research task does not exist.
export class LinkupTaskNotFoundError extends LinkupError {
Expand Down Expand Up @@ -170,6 +182,54 @@ export class LinkupPaymentRequiredError extends LinkupError {
}
}

// Payment payload invalid error, raised when the Linkup API rejects the provided x402 payment payload.
export class LinkupPaymentPayloadInvalidError extends LinkupPaymentRequiredError {
constructor(message?: string) {
super(message);
this.name = LinkupPaymentPayloadInvalidError.name;

if ('captureStackTrace' in Error) {
Error.captureStackTrace(this, LinkupPaymentPayloadInvalidError);
}
}
}

// Payment verification failed error, raised when the Linkup API cannot verify the x402 payment.
export class LinkupPaymentVerificationFailedError extends LinkupPaymentRequiredError {
constructor(message?: string) {
super(message);
this.name = LinkupPaymentVerificationFailedError.name;

if ('captureStackTrace' in Error) {
Error.captureStackTrace(this, LinkupPaymentVerificationFailedError);
}
}
}

// Payment settlement failed error, raised when the Linkup API cannot settle the x402 payment.
export class LinkupPaymentSettlementFailedError extends LinkupPaymentRequiredError {
constructor(message?: string) {
super(message);
this.name = LinkupPaymentSettlementFailedError.name;

if ('captureStackTrace' in Error) {
Error.captureStackTrace(this, LinkupPaymentSettlementFailedError);
}
}
}

// Unsupported task type error, raised when the Linkup API rejects a task type for the organization.
export class LinkupUnsupportedTaskTypeError extends LinkupError {
constructor(message?: string) {
super(message);
this.name = LinkupUnsupportedTaskTypeError.name;

if ('captureStackTrace' in Error) {
Error.captureStackTrace(this, LinkupUnsupportedTaskTypeError);
}
}
}

// Backward-compatible alias for older platform deployments.
export class FetchUrlIsFileError extends LinkupFetchUnsupportedContentTypeError {
constructor(message?: string) {
Expand Down
20 changes: 18 additions & 2 deletions src/utils/refine-error.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import {
LinkupFetchUnsupportedContentTypeError,
LinkupInsufficientCreditError,
LinkupInvalidRequestError,
LinkupIpNotWhitelistedError,
LinkupNoResultError,
LinkupPaymentPayloadInvalidError,
LinkupPaymentRequiredError,
LinkupPaymentSettlementFailedError,
LinkupPaymentVerificationFailedError,
LinkupTaskNotFoundError,
LinkupTasksQueueLimitExceededError,
LinkupTooManyRequestsError,
LinkupUnknownError,
LinkupUnsupportedTaskTypeError,
} from '../errors';
import type { LinkupApiError } from '../types';

Expand Down Expand Up @@ -53,13 +58,24 @@ export const refineError = (e: LinkupApiError): LinkupError => {
return new LinkupInvalidRequestError(concatErrorAndDetails(e));
}
case 402:
return new LinkupPaymentRequiredError(message);
switch (code) {
case 'X402_PAYMENT_PAYLOAD_INVALID':
return new LinkupPaymentPayloadInvalidError(message);
case 'X402_PAYMENT_VERIFICATION_FAILED':
return new LinkupPaymentVerificationFailedError(message);
case 'X402_PAYMENT_SETTLEMENT_FAILED':
return new LinkupPaymentSettlementFailedError(message);
default:
return new LinkupPaymentRequiredError(message);
}
case 401:
return new LinkupAuthenticationError(message);
case 403:
switch (code) {
case 'IP_NOT_WHITELISTED':
return new LinkupIpNotWhitelistedError(message);
case 'TASK_TYPE_NOT_SUPPORTED':
return new LinkupUnknownError(`Unsupported task type: ${message}`);
return new LinkupUnsupportedTaskTypeError(message);
default:
return new LinkupAuthenticationError(message);
}
Expand Down