Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated Jest configuration to properly handle ESM modules from node-fetch v3
- Removed incompatible AbortSignal import from node-fetch externals (now uses native Node.js AbortSignal)

### Added
- Expose raw response headers on all responses via non-enumerable `rawHeaders` while keeping existing `headers` camelCased

## [7.11.0] - 2025-06-23

### Added
Expand Down
9 changes: 9 additions & 0 deletions examples/messages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ The file is structured with:
### Basic Messages (`messages.ts`)
Shows basic message operations including reading, sending, and drafting messages.

### Accessing Rate Limit Headers
All SDK responses now expose a non-enumerable `rawHeaders` with dashed lowercase keys so you can read rate limit information:

```ts
const res = await nylas.messages.list({ identifier: process.env.NYLAS_GRANT_ID!, queryParams: { limit: 1 } });
const limit = res.rawHeaders?.['x-rate-limit-limit'];
const remaining = res.rawHeaders?.['x-rate-limit-remaining'];
```

## Quick Start

### 1. Set up environment
Expand Down
21 changes: 16 additions & 5 deletions src/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,22 @@ export default class APIClient {
const text = await response.text();

try {
const responseJSON = JSON.parse(text);
// Inject the flow ID and headers into the response
responseJSON.flowId = flowId;
responseJSON.headers = headers;
return objKeysToCamelCase(responseJSON, ['metadata']);
const parsed = JSON.parse(text);
const payload = objKeysToCamelCase(
{
...parsed,
flowId,
// deprecated: headers will be removed in a future release. This is for backwards compatibility.
headers,
},
['metadata']
);
// Attach rawHeaders as a non-enumerable property to avoid breaking deep equality
Object.defineProperty(payload, 'rawHeaders', {
value: headers,
enumerable: false,
});
return payload;
} catch (e) {
throw new Error(`Could not parse response from the server: ${text}`);
}
Expand Down
33 changes: 33 additions & 0 deletions src/models/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
*/
export interface NylasBaseResponse {
requestId: string;
/**
* The flow ID
* Provide this to Nylas support to help trace requests and responses
*/
flowId?: string;
/**
* The response headers with camelCased keys (backwards compatible)
* @deprecated Use rawHeaders instead
*/
headers?: Record<string, string>;
/**
* The raw response headers with original dashed lowercase keys
*/
rawHeaders?: Record<string, string>;
}

/**
Expand All @@ -24,8 +38,13 @@ export interface NylasResponse<T> {
flowId?: string;
/**
* The response headers
* @deprecated Use rawHeaders instead
*/
headers?: Record<string, string>;
/**
* The raw response headers with original dashed lowercase keys
*/
rawHeaders?: Record<string, string>;
}

/**
Expand All @@ -44,6 +63,20 @@ export interface NylasListResponse<T> {
* The cursor to use to get the next page of data.
*/
nextCursor?: string;
/**
* The flow ID
* Provide this to Nylas support to help trace requests and responses
*/
flowId?: string;
/**
* The response headers with camelCased keys (backwards compatible)
* @deprecated Use rawHeaders instead
*/
headers?: Record<string, string>;
/**
* The raw response headers with original dashed lowercase keys
*/
rawHeaders?: Record<string, string>;
}

/**
Expand Down
68 changes: 68 additions & 0 deletions tests/apiClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,40 @@ describe('APIClient', () => {
mockHeaders['x-request-id']
);
});

it('should include rawHeaders with dashed lowercase keys', async () => {
const mockFlowId = 'test-flow-raw-123';
const mockHeaders = {
'x-request-id': 'req-raw-123',
'x-nylas-api-version': 'v3',
'x-rate-limit-limit': '100',
'x-rate-limit-remaining': '99',
};

const payload = {
id: 456,
name: 'raw-test',
};

const mockResp = mockResponse(JSON.stringify(payload));
mockResp.headers.set('x-fastly-id', mockFlowId);
Object.entries(mockHeaders).forEach(([key, value]) => {
mockResp.headers.set(key, value);
});

const result = await client.requestWithResponse(mockResp);

expect((result as any).rawHeaders).toBeDefined();
expect((result as any).rawHeaders['x-fastly-id']).toBe(mockFlowId);
expect((result as any).rawHeaders['x-request-id']).toBe(
mockHeaders['x-request-id']
);
expect((result as any).rawHeaders['x-nylas-api-version']).toBe(
mockHeaders['x-nylas-api-version']
);
expect((result as any).rawHeaders['x-rate-limit-limit']).toBe('100');
expect((result as any).rawHeaders['x-rate-limit-remaining']).toBe('99');
});
});

describe('request', () => {
Expand Down Expand Up @@ -278,6 +312,40 @@ describe('APIClient', () => {
expect((response as any).headers['xFastlyId']).toBe(mockFlowId);
});

it('should include rawHeaders on standard responses', async () => {
const mockFlowId = 'test-flow-raw-abc';
const mockHeaders = {
'x-request-id': 'req-raw-abc',
'x-nylas-api-version': 'v3',
'x-rate-limit-limit': '200',
};

const payload = {
id: 789,
name: 'raw',
};

const mockResp = mockResponse(JSON.stringify(payload));
mockResp.headers.set('x-fastly-id', mockFlowId);
Object.entries(mockHeaders).forEach(([key, value]) => {
mockResp.headers.set(key, value);
});

fetchMock.mockImplementationOnce(() => Promise.resolve(mockResp));

const response = await client.request({ path: '/test', method: 'GET' });

expect((response as any).rawHeaders).toBeDefined();
expect((response as any).rawHeaders['x-fastly-id']).toBe(mockFlowId);
expect((response as any).rawHeaders['x-request-id']).toBe(
mockHeaders['x-request-id']
);
expect((response as any).rawHeaders['x-nylas-api-version']).toBe(
mockHeaders['x-nylas-api-version']
);
expect((response as any).rawHeaders['x-rate-limit-limit']).toBe('200');
});

it('should throw an error if the response is undefined', async () => {
fetchMock.mockImplementationOnce(() =>
Promise.resolve(undefined as any)
Expand Down