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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/generate-random-data/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"strict": false,
"esModuleInterop": true,
"allowJs": true,
"checkJs": false,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true
},
"include": ["**/*.ts", "**/*.js"],
"ts-node": {
"esm": true
}
}
26 changes: 25 additions & 1 deletion src/api-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6366,7 +6366,11 @@ type FileUploadWithOptionalNameRequest = {
name?: StringRequest
}

type PageIconRequest = FileUploadPageIconRequest | EmojiPageIconRequest
type PageIconRequest =
| FileUploadPageIconRequest
| EmojiPageIconRequest
| ExternalPageIconRequest
| CustomEmojiPageIconRequest

type PageCoverRequest = FileUploadPageCoverRequest | ExternalPageCoverRequest

Expand Down Expand Up @@ -7454,6 +7458,26 @@ type EmojiPageIconRequest = {
emoji: EmojiRequest
}

type ExternalPageIconRequest = {
type?: "external"
external: {
// The URL of the external file.
url: string
}
}

type CustomEmojiPageIconRequest = {
type?: "custom_emoji"
custom_emoji: {
// The ID of the custom emoji.
id: IdRequest
// The name of the custom emoji.
name?: string
// The URL of the custom emoji.
url?: string
}
}

type FileUploadPageCoverRequest = {
type?: "file_upload"
// The file upload for the cover.
Expand Down
20 changes: 16 additions & 4 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export class RequestTimeoutError extends NotionClientErrorBase<ClientErrorCode.R

type HTTPResponseErrorCode = ClientErrorCode.ResponseError | APIErrorCode

type AdditionalData = Record<string, string | string[]>

class HTTPResponseError<
Code extends HTTPResponseErrorCode
> extends NotionClientErrorBase<Code> {
Expand All @@ -127,20 +129,23 @@ class HTTPResponseError<
readonly status: number
readonly headers: SupportedResponse["headers"]
readonly body: string
readonly additional_data: AdditionalData | undefined

constructor(args: {
code: Code
status: number
message: string
headers: SupportedResponse["headers"]
rawBodyText: string
additional_data: AdditionalData | undefined
}) {
super(args.message)
const { code, status, headers, rawBodyText } = args
const { code, status, headers, rawBodyText, additional_data } = args
this.code = code
this.status = status
this.headers = headers
this.body = rawBodyText
this.additional_data = additional_data
}
}

Expand Down Expand Up @@ -193,6 +198,7 @@ export class UnknownHTTPResponseError extends HTTPResponseError<ClientErrorCode.
message:
args.message ??
`Request to Notion API failed with status: ${args.status}`,
additional_data: undefined,
})
}

Expand Down Expand Up @@ -243,6 +249,7 @@ export function buildRequestError(
headers: response.headers,
status: response.status,
rawBodyText: bodyText,
additional_data: apiErrorResponseBody.additional_data,
})
}
return new UnknownHTTPResponseError({
Expand All @@ -253,9 +260,13 @@ export function buildRequestError(
})
}

function parseAPIErrorResponseBody(
body: string
): { code: APIErrorCode; message: string } | undefined {
function parseAPIErrorResponseBody(body: string):
| {
code: APIErrorCode
message: string
additional_data: AdditionalData | undefined
}
| undefined {
if (typeof body !== "string") {
return
}
Expand All @@ -279,6 +290,7 @@ function parseAPIErrorResponseBody(
...parsed,
code: parsed["code"],
message: parsed["message"],
additional_data: parsed["additional_data"] as AdditionalData | undefined,
Copy link

Copilot AI Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a type assertion as AdditionalData | undefined bypasses type checking and could lead to runtime errors if the API returns additional_data with unexpected structure. Consider implementing proper validation or using a more permissive type definition.

Suggested change
additional_data: parsed["additional_data"] as AdditionalData | undefined,
additional_data: isAdditionalData(parsed["additional_data"]) ? parsed["additional_data"] : undefined,
Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't really think of a good way to do a runtime check of the keys of an object being strings in JS :/ I feel like this is a necessary evil here (at the boundary of the web API call interface) and we kind of have to do some level of casting or ugly code and I chose the casting 😅)

}
}

Expand Down
46 changes: 45 additions & 1 deletion test/Client.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert = require("assert")
import { Client } from "../src"
import { APIResponseError, Client } from "../src"

describe("Notion SDK Client", () => {
it("Constructs without throwing", () => {
Expand Down Expand Up @@ -136,5 +136,49 @@ describe("Notion SDK Client", () => {
})
)
})

it("parses additional_data from API validation error response", async () => {
mockFetch.mockResolvedValue({
ok: false,
text: () =>
Promise.resolve(
JSON.stringify({
code: "validation_error",
message:
"Databases with multiple data sources are not supported in this API version.",
object: "error",
status: 400,
additional_data: {
error_type: "multiple_data_sources_for_database",
database_id: "123",
child_data_source_ids: ["456", "789"],
minimum_api_version: "2025-09-03",
},
})
),
headers: new Headers(),
status: 400,
} as Response)

try {
await notion.databases.retrieve({
database_id: "123",
})
assert.fail("Expected error to be thrown")
} catch (error) {
assert(error instanceof APIResponseError)
expect(error.code).toEqual("validation_error")
expect(error.status).toEqual(400)
expect(error.message).toEqual(
"Databases with multiple data sources are not supported in this API version."
)
expect(error.additional_data).toEqual({
error_type: "multiple_data_sources_for_database",
database_id: "123",
child_data_source_ids: ["456", "789"],
minimum_api_version: "2025-09-03",
})
}
})
})
})