///|
/// Error handling, ported from `src/errors.ts`.
///
/// Every failure the client throws is a `NotionClientError` wrapping a
/// `NotionError` record. The record keeps the same fields the TypeScript SDK
/// exposes: `code`, `message`, `status`, `headers`, `body`, `additional_data`,
/// `request_id` and `ray_id`.
///|
/// Error codes returned in responses from the API.
pub(all) enum APIErrorCode {
Unauthorized
RestrictedResource
ObjectNotFound
RateLimited
InvalidJson
InvalidRequestUrl
InvalidRequest
InvalidBeta
ValidationError
ConflictError
InternalServerError
ServiceOverload
ServiceUnavailable
GatewayTimeout
}
///|
/// The wire value of an API error code.
pub fn APIErrorCode::to_string(self : APIErrorCode) -> String {
match self {
Unauthorized => "unauthorized"
RestrictedResource => "restricted_resource"
ObjectNotFound => "object_not_found"
RateLimited => "rate_limited"
InvalidJson => "invalid_json"
InvalidRequestUrl => "invalid_request_url"
InvalidRequest => "invalid_request"
InvalidBeta => "invalid_beta"
ValidationError => "validation_error"
ConflictError => "conflict_error"
InternalServerError => "internal_server_error"
ServiceOverload => "service_overload"
ServiceUnavailable => "service_unavailable"
GatewayTimeout => "gateway_timeout"
}
}
///|
/// Parses an API error code from its wire value.
pub fn APIErrorCode::from_string(value : String) -> APIErrorCode? {
match value {
"unauthorized" => Some(Unauthorized)
"restricted_resource" => Some(RestrictedResource)
"object_not_found" => Some(ObjectNotFound)
"rate_limited" => Some(RateLimited)
"invalid_json" => Some(InvalidJson)
"invalid_request_url" => Some(InvalidRequestUrl)
"invalid_request" => Some(InvalidRequest)
"invalid_beta" => Some(InvalidBeta)
"validation_error" => Some(ValidationError)
"conflict_error" => Some(ConflictError)
"internal_server_error" => Some(InternalServerError)
"service_overload" => Some(ServiceOverload)
"service_unavailable" => Some(ServiceUnavailable)
"gateway_timeout" => Some(GatewayTimeout)
_ => None
}
}
///|
/// Whether a string is a known API error code.
pub fn is_api_error_code(value : String) -> Bool {
APIErrorCode::from_string(value) is Some(_)
}
///|
/// Error codes generated for client errors.
pub(all) enum ClientErrorCode {
RequestTimeout
ResponseError
InvalidPathParameter
}
///|
/// The wire value of a client error code.
pub fn ClientErrorCode::to_string(self : ClientErrorCode) -> String {
match self {
RequestTimeout => "notionhq_client_request_timeout"
ResponseError => "notionhq_client_response_error"
InvalidPathParameter => "notionhq_client_invalid_path_parameter"
}
}
///|
/// The kind of error, mirroring the TypeScript SDK's error classes.
pub(all) enum NotionErrorKind {
RequestTimeoutError
UnknownHTTPResponseError
ApiResponseError
InvalidPathParameterError
BrowserTokenNotAllowedError
}
///|
/// The name of the error class a kind corresponds to.
pub fn NotionErrorKind::name(self : NotionErrorKind) -> String {
match self {
RequestTimeoutError => "RequestTimeoutError"
UnknownHTTPResponseError => "UnknownHTTPResponseError"
ApiResponseError => "APIResponseError"
InvalidPathParameterError => "InvalidPathParameterError"
BrowserTokenNotAllowedError => "BrowserTokenNotAllowedError"
}
}
///|
/// The fields carried by every error the client raises.
pub(all) struct NotionError {
kind : NotionErrorKind
code : String
message : String
status : Int
headers : Array[(String, String)]
body : String
additional_data : Json?
request_id : String?
ray_id : String?
}
///|
/// Builds a `NotionError`. `status`, `headers` and `body` default to the
/// values used by errors that never reached the API.
pub fn NotionError::new(
kind~ : NotionErrorKind,
code~ : String,
message~ : String,
status? : Int = 0,
headers? : Array[(String, String)] = [],
body? : String = "",
additional_data? : Json? = None,
request_id? : String? = None,
ray_id? : String? = None,
) -> NotionError {
{
kind,
code,
message,
status,
headers,
body,
additional_data,
request_id,
ray_id,
}
}
///|
/// The error class name, e.g. `"APIResponseError"`.
pub fn NotionError::name(self : NotionError) -> String {
self.kind.name()
}
///|
/// Error thrown by the client if a request times out.
pub fn request_timeout_error(
message? : String = "Request to Notion API has timed out",
) -> NotionError {
NotionError::new(
kind=RequestTimeoutError,
code=ClientErrorCode::RequestTimeout.to_string(),
message~,
)
}
///|
/// Error thrown when a path parameter contains invalid characters such as path
/// traversal sequences (`..`) that could alter the intended API endpoint.
pub fn invalid_path_parameter_error(
message? : String = "Path parameter contains invalid characters that could alter the request path",
) -> NotionError {
NotionError::new(
kind=InvalidPathParameterError,
code=ClientErrorCode::InvalidPathParameter.to_string(),
message~,
)
}
///|
/// Error thrown when the client would send a token or client secret from a
/// browser page without `dangerouslyAllowBrowser: true`.
pub fn browser_token_not_allowed_error() -> NotionError {
NotionError::new(
kind=BrowserTokenNotAllowedError,
code="browser_token_not_allowed",
message="It looks like you're running in a browser. Using a Notion token here " +
"is blocked by default: anyone who can load the page can read the token and " +
"act as your connection. Keep the token on a server instead. If only you can " +
"open this page and you accept the risk, pass `dangerouslyAllowBrowser: true` " +
"to the Client.",
)
}
///|
/// A response from the API indicating a problem.
pub fn api_response_error(
code~ : APIErrorCode,
message~ : String,
status~ : Int,
headers~ : Array[(String, String)],
body~ : String,
additional_data? : Json? = None,
request_id? : String? = None,
) -> NotionError {
NotionError::new(
kind=ApiResponseError,
code=code.to_string(),
message~,
status~,
headers~,
body~,
additional_data~,
request_id~,
ray_id=get_response_header(headers, "cf-ray"),
)
}
///|
/// Error thrown if an API call responds with an unknown error code, or does not
/// respond with a property-formatted error.
pub fn unknown_http_response_error(
status~ : Int,
headers~ : Array[(String, String)],
body~ : String,
message? : String? = None,
) -> NotionError {
let ray_id = get_response_header(headers, "cf-ray")
let request_id = get_response_header(headers, "x-notion-request-id")
let message = match message {
Some(m) => m
None =>
build_unknown_response_message(
status~,
content_type=get_response_header(headers, "content-type"),
ray_id~,
request_id~,
)
}
NotionError::new(
kind=UnknownHTTPResponseError,
code=ClientErrorCode::ResponseError.to_string(),
message~,
status~,
headers~,
body~,
additional_data=None,
request_id~,
ray_id~,
)
}
///|
/// The error type that encompasses every failure the Notion client raises.
pub(all) suberror NotionClientError {
NotionClientError(NotionError)
}
///|
/// `true` if the error is a `NotionClientError`.
pub fn is_notion_client_error(error : Error) -> Bool {
match error {
NotionClientError(_) => true
_ => false
}
}
///|
/// `true` if the error is an HTTP response error
/// (`APIResponseError` or `UnknownHTTPResponseError`).
pub fn is_http_response_error(error : Error) -> Bool {
match error {
NotionClientError(e) =>
e.kind is ApiResponseError || e.kind is UnknownHTTPResponseError
_ => false
}
}
///|
/// `true` if the error is an `APIResponseError`.
pub fn is_api_response_error(error : Error) -> Bool {
match error {
NotionClientError(e) => e.kind is ApiResponseError
_ => false
}
}
///|
/// Reads a header by name, case-insensitively per RFC 9110.
pub fn get_response_header(
headers : Array[(String, String)],
name : String,
) -> String? {
let lower_name = name.to_lower()
for pair in headers {
let (key, value) = pair
if key.to_lower() == lower_name {
return Some(value)
}
}
None
}
///|
/// Validates that a request path does not contain path traversal sequences.
/// Raises `InvalidPathParameterError` if the path contains `..` segments,
/// including URL-encoded variants like `%2e%2e`.
pub fn validate_request_path(path : String) -> Unit raise {
if path.contains("..") {
raise NotionClientError(
invalid_path_parameter_error(
message="Request path \"" +
path +
"\" contains path traversal sequence \"..\"",
),
)
}
let lower = path.to_lower()
if lower.contains("%2e") {
let decoded = lower.replace_all(old="%2e", new=".")
if decoded.contains("..") {
raise NotionClientError(
invalid_path_parameter_error(
message="Request path \"" +
path +
"\" contains encoded path traversal sequence",
),
)
}
}
}
///|
/// Builds the default message for an unrecognized HTTP response.
///
/// A response with a Cloudflare Ray ID but no Notion request ID was answered
/// before it reached the API.
pub fn build_unknown_response_message(
status~ : Int,
content_type~ : String?,
ray_id~ : String?,
request_id~ : String?,
) -> String {
let base = "Request to Notion API failed with status: " + status.to_string()
match ray_id {
None => base
Some(ray) =>
match request_id {
Some(_) => base
None => {
let content_type_note = match content_type {
Some(ct) => " (content-type: " + ct + ")"
None => ""
}
let blocked_note = if status == 403 {
" This may mean the request was blocked by a network security rule."
} else {
""
}
base +
". The response was returned by Notion's edge proxy before reaching " +
"the Notion API" +
content_type_note +
"." +
blocked_note +
" Cloudflare Ray ID: " +
ray +
". Include this ID when contacting Notion support."
}
}
}
}
///|
/// Builds the error to throw for a non-ok HTTP response, preferring the API's
/// structured error body over a generic response error.
pub fn build_request_error(
response : HttpResponse,
body_text : String,
) -> NotionClientError {
match parse_api_error_response_body(body_text) {
Some(parsed) =>
NotionClientError(
api_response_error(
code=parsed.code,
message=parsed.message,
status=response.status,
headers=response.headers,
body=body_text,
additional_data=parsed.additional_data,
request_id=parsed.request_id,
),
)
None =>
NotionClientError(
unknown_http_response_error(
status=response.status,
headers=response.headers,
body=body_text,
message=None,
),
)
}
}
///|
/// The structured shape of an API error response body.
pub(all) struct ParsedAPIError {
code : APIErrorCode
message : String
additional_data : Json?
request_id : String?
}
///|
/// Parses a Notion API error body. Returns `None` when the body is not JSON or
/// does not carry a `message` and a known `code`.
pub fn parse_api_error_response_body(body : String) -> ParsedAPIError? {
let parsed : Json = @json.parse(body) catch { _ => return None }
match parsed {
Json::Object(fields) =>
match (fields.get("message"), fields.get("code")) {
(Some(Json::String(message)), Some(Json::String(code))) =>
match APIErrorCode::from_string(code) {
Some(code) =>
Some({
code,
message,
additional_data: fields.get("additional_data"),
request_id: match fields.get("request_id") {
Some(Json::String(id)) => Some(id)
_ => None
},
})
None => None
}
_ => None
}
_ => None
}
}