@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
197 lines (168 loc) • 5.99 kB
text/typescript
import type {HttpContext} from 'get-it'
import type {ActionError, Any, ErrorProps, MutationError, QueryParseError} from '../types'
import {codeFrame} from '../util/codeFrame'
import {isRecord} from '../util/isRecord'
const MAX_ITEMS_IN_ERROR_MESSAGE = 5
/** @public */
export class ClientError extends Error {
response: ErrorProps['response']
statusCode: ErrorProps['statusCode'] = 400
responseBody: ErrorProps['responseBody']
details: ErrorProps['details']
constructor(res: Any, context?: HttpContext) {
const props = extractErrorProps(res, context)
super(props.message)
Object.assign(this, props)
}
}
/** @public */
export class ServerError extends Error {
response: ErrorProps['response']
statusCode: ErrorProps['statusCode'] = 500
responseBody: ErrorProps['responseBody']
details: ErrorProps['details']
constructor(res: Any) {
const props = extractErrorProps(res)
super(props.message)
Object.assign(this, props)
}
}
function extractErrorProps(res: Any, context?: HttpContext): ErrorProps {
const body = res.body
const props = {
response: res,
statusCode: res.statusCode,
responseBody: stringifyBody(body, res),
message: '',
details: undefined as Any,
}
// Fall back early if we didn't get a JSON object returned as expected
if (!isRecord(body)) {
props.message = httpErrorMessage(res, body)
return props
}
const error = body.error
// API/Boom style errors ({statusCode, error, message})
if (typeof error === 'string' && typeof body.message === 'string') {
props.message = `${error} - ${body.message}`
return props
}
// Content Lake errors with a `error` prop being an object
if (typeof error !== 'object' || error === null) {
if (typeof error === 'string') {
props.message = error
} else if (typeof body.message === 'string') {
props.message = body.message
} else {
props.message = httpErrorMessage(res, body)
}
return props
}
// Mutation errors (specifically)
if (isMutationError(error) || isActionError(error)) {
const allItems = error.items || []
const items = allItems
.slice(0, MAX_ITEMS_IN_ERROR_MESSAGE)
.map((item) => item.error?.description)
.filter(Boolean)
let itemsStr = items.length ? `:\n- ${items.join('\n- ')}` : ''
if (allItems.length > MAX_ITEMS_IN_ERROR_MESSAGE) {
itemsStr += `\n...and ${allItems.length - MAX_ITEMS_IN_ERROR_MESSAGE} more`
}
props.message = `${error.description}${itemsStr}`
props.details = body.error
return props
}
// Query parse errors
if (isQueryParseError(error)) {
const tag = context?.options?.query?.tag
props.message = formatQueryParseError(error, tag)
props.details = body.error
return props
}
if ('description' in error && typeof error.description === 'string') {
// Query/database errors ({error: {description, other, arb, props}})
props.message = error.description
props.details = error
return props
}
// Other, more arbitrary errors
props.message = httpErrorMessage(res, body)
return props
}
function isMutationError(error: object): error is MutationError {
return (
'type' in error &&
error.type === 'mutationError' &&
'description' in error &&
typeof error.description === 'string'
)
}
function isActionError(error: object): error is ActionError {
return (
'type' in error &&
error.type === 'actionError' &&
'description' in error &&
typeof error.description === 'string'
)
}
/** @internal */
export function isQueryParseError(error: object): error is QueryParseError {
return (
isRecord(error) &&
error.type === 'queryParseError' &&
typeof error.query === 'string' &&
typeof error.start === 'number' &&
typeof error.end === 'number'
)
}
/**
* Formats a GROQ query parse error into a human-readable string.
*
* @param error - The error object containing details about the parse error.
* @param tag - An optional tag to include in the error message.
* @returns A formatted error message string.
* @public
*/
export function formatQueryParseError(error: QueryParseError, tag?: string | null) {
const {query, start, end, description} = error
if (!query || typeof start === 'undefined') {
return `GROQ query parse error: ${description}`
}
const withTag = tag ? `\n\nTag: ${tag}` : ''
const framed = codeFrame(query, {start, end}, description)
return `GROQ query parse error:\n${framed}${withTag}`
}
function httpErrorMessage(res: Any, body: unknown) {
const details = typeof body === 'string' ? ` (${sliceWithEllipsis(body, 100)})` : ''
const statusMessage = res.statusMessage ? ` ${res.statusMessage}` : ''
return `${res.method}-request to ${res.url} resulted in HTTP ${res.statusCode}${statusMessage}${details}`
}
function stringifyBody(body: Any, res: Any) {
const contentType = (res.headers['content-type'] || '').toLowerCase()
const isJson = contentType.indexOf('application/json') !== -1
return isJson ? JSON.stringify(body, null, 2) : body
}
function sliceWithEllipsis(str: string, max: number) {
return str.length > max ? `${str.slice(0, max)}…` : str
}
/** @public */
export class CorsOriginError extends Error {
projectId: string
addOriginUrl?: URL
constructor({projectId}: {projectId: string}) {
super('CorsOriginError')
this.name = 'CorsOriginError'
this.projectId = projectId
const url = new URL(`https://sanity.io/manage/project/${projectId}/api`)
if (typeof location !== 'undefined') {
const {origin} = location
url.searchParams.set('cors', 'add')
url.searchParams.set('origin', origin)
this.addOriginUrl = url
this.message = `The current origin is not allowed to connect to the Live Content API. Add it here: ${url}`
} else {
this.message = `The current origin is not allowed to connect to the Live Content API. Change your configuration here: ${url}`
}
}
}