Skip to main content

postrust_core/
error.rs

1//! Error types for Postrust.
2//!
3//! Provides comprehensive error handling with HTTP status code mapping.
4
5use http::StatusCode;
6use thiserror::Error;
7
8/// Result type for Postrust operations.
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Main error type for Postrust.
12#[derive(Error, Debug)]
13pub enum Error {
14    // ========================================================================
15    // Request Parsing Errors (4xx)
16    // ========================================================================
17    #[error("Invalid path: {0}")]
18    InvalidPath(String),
19
20    /// An aggregate was requested while `db-aggregates-enabled` is off.
21    ///
22    /// Off is the default, matching PostgREST: an unrestricted aggregate over
23    /// a large table is an easy way to make a server do far more work than the
24    /// request looks like it asks for.
25    #[error("Use of aggregate functions is not allowed")]
26    AggregatesNotAllowed,
27
28    #[error("Invalid query parameter: {0}")]
29    InvalidQueryParam(String),
30
31    /// A fragment of the query string the grammar could not read.
32    ///
33    /// Reported the way a parser reports: what was being read, the text as the
34    /// client wrote it, where reading stopped, and what would have been
35    /// accepted there. All four are the client's own input or the API's own
36    /// grammar, so none of it is a disclosure -- and without them the client
37    /// is told only that something in a URL it wrote is wrong.
38    #[error("\"failed to parse {kind} ({text})\" (line 1, column {column})")]
39    UnparsableQuery {
40        /// What was being read: `filter`, `logic tree`, `order`.
41        kind: &'static str,
42        /// The fragment, as it was written.
43        text: String,
44        /// Where reading stopped, counting from one.
45        column: usize,
46        /// What was found there and what would have been accepted.
47        expected: String,
48    },
49
50    #[error("Invalid header: {0}")]
51    InvalidHeader(&'static str),
52
53    #[error("{0}")]
54    InvalidBody(String),
55
56    #[error("Unsupported HTTP method: {0}")]
57    UnsupportedMethod(String),
58
59    /// A method a function cannot be called with.
60    ///
61    /// A function is invoked with GET, HEAD or POST. The others are refused
62    /// with a message that says so rather than one about the method in
63    /// general, since the method is fine elsewhere in the API.
64    #[error("Cannot use the {0} method on RPC")]
65    InvalidRpcMethod(String),
66
67    /// A profile header named a schema the server does not expose.
68    ///
69    /// Carries the schemas that *are* exposed, because the client needs them
70    /// to correct the request and they are part of the API's own contract.
71    #[error("Invalid schema: {requested}")]
72    UnacceptableSchema {
73        requested: String,
74        exposed: Vec<String>,
75    },
76
77    #[error("Could not find the '{column}' column of '{relation}' in the schema cache")]
78    UnknownColumn {
79        /// The column the request named.
80        column: String,
81        /// The relation it was looked for on.
82        relation: String,
83    },
84
85    #[error("Requested range not satisfiable")]
86    InvalidRange(String),
87
88    #[error("None of these media types are available: {0}")]
89    InvalidMediaType(String),
90
91    #[error("Missing required parameter: {0}")]
92    MissingParameter(String),
93
94    #[error("Ambiguous request: {0}")]
95    AmbiguousRequest(String),
96
97    /// Several signatures of a function fit the arguments equally well.
98    #[error("Could not choose the best candidate function between: {}", .candidates.join(", "))]
99    AmbiguousFunction { candidates: Vec<String> },
100
101    /// Several relationships connect the two resources and none was named.
102    #[error("Could not embed because more than one relationship was found for '{origin}' and '{target}'")]
103    AmbiguousRelationship {
104        origin: String,
105        target: String,
106        /// Each candidate as `(cardinality, description)`.
107        candidates: Vec<(String, String)>,
108        /// How the client would name each candidate: `big_projects!jobs`.
109        disambiguated: Vec<String>,
110    },
111
112    // ========================================================================
113    // Authentication/Authorization Errors (401/403)
114    // ========================================================================
115    /// The token could not be decoded or its signature did not verify.
116    ///
117    /// Distinct from a claims error: nothing about the token was readable, so
118    /// there is nothing to say about what it claimed.
119    #[error("{0}")]
120    InvalidJwt(String),
121
122    /// No key this server holds could decode the token.
123    ///
124    /// Its own code would be indistinguishable from any other decode failure,
125    /// so what separates it is the detail: the client is being told to look at
126    /// which key signed the token, not at the token's contents.
127    #[error("No suitable key or wrong key type")]
128    NoSuitableJwtKey,
129
130    /// The server is configured to read tokens and has nothing to read them
131    /// with. A fault in the deployment, not in the request.
132    #[error("Server lacks JWT secret")]
133    JwtSecretMissing,
134
135    /// The token decoded, but a claim was missing, malformed or unsatisfied.
136    #[error("{0}")]
137    JwtClaim(String),
138
139    #[error("Anonymous access is disabled")]
140    MissingAuth,
141
142    #[error("Insufficient permissions: {0}")]
143    InsufficientPermissions(String),
144
145    // ========================================================================
146    // Resource Errors (404)
147    // ========================================================================
148    #[error("Resource not found: {0}")]
149    NotFound(String),
150
151    #[error("Table not found: {name}")]
152    TableNotFound {
153        /// The name the request asked for, schema-qualified.
154        name: String,
155        /// A table of a very similar name, where the schema has one.
156        suggestion: Option<String>,
157    },
158
159    /// No function of that name accepts the arguments supplied.
160    ///
161    /// Carries what was looked for so the client can see why nothing matched:
162    /// the name, the argument names it was called with, and the signature of
163    /// an overload that does exist, where one does.
164    #[error("Could not find the function {name}")]
165    FunctionNotFound {
166        /// The function's qualified name, without arguments.
167        name: String,
168        /// The argument names it was called with.
169        params: Vec<String>,
170        /// A signature that does exist, where one does.
171        candidate: Option<String>,
172        /// The type of a single unnamed parameter that would also have
173        /// matched, spelled as the message spells it.
174        ///
175        /// Over `POST` the whole body can be one argument, and which type it
176        /// could be is decided by the body's media type: `json/jsonb` for a
177        /// JSON body, `text`, `xml` or `bytea` for the others. Naming it says
178        /// what else was looked for besides the arguments themselves.
179        single_param: Option<String>,
180    },
181
182    /// A `RAISE sqlstate 'PGRST'` the schema wrote wrongly.
183    ///
184    /// The message and the detail are both JSON documents with required keys,
185    /// and a function that gets one wrong has not asked for a response -- it
186    /// has a bug. Reporting it as the 500 it is says so; applying half of what
187    /// it asked for would not.
188    #[error("Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error")]
189    RaiseNotUnderstood(RaiseFault),
190
191    /// `Accept: application/geo+json` on rows that carry no geometry.
192    ///
193    /// PostGIS raises this from `ST_AsGeoJSON` on a record with no geometry
194    /// column, and it is a database error rather than one of PostgREST's own
195    /// -- so it keeps the SQLSTATE it came with.
196    #[error("geometry column is missing")]
197    MissingGeometryColumn,
198
199    #[error("Column not found: {0}")]
200    ColumnNotFound(String),
201
202    #[error("Could not find a relationship between '{origin}' and '{target}' in the schema cache")]
203    RelationshipNotFound {
204        /// The table the embedding started from.
205        origin: String,
206        /// The resource the request asked to embed.
207        target: String,
208        /// The `!hint` that was meant to identify it, if any.
209        hint: Option<String>,
210        /// The schema that was searched.
211        schema: String,
212        /// A relationship of a very similar name, where there is one.
213        suggestion: Option<String>,
214    },
215
216    /// A filter or order names something the request never embedded.
217    ///
218    /// `?non_existent.name=like.*x*` is not a column with a dot in it, and
219    /// answering it as though the prefix were meaningless would silently
220    /// return the unfiltered table.
221    #[error("'{0}' is not an embedded resource in this request")]
222    NotAnEmbeddedResource(String),
223
224    /// Ordering by a column of a resource that yields many rows per parent.
225    ///
226    /// There is no single value to order on: the parent has a list.
227    #[error("A related order on '{relation}' is not possible")]
228    RelatedOrderNotPossible {
229        /// The table the request started from.
230        origin: String,
231        /// The embedded resource the order named.
232        relation: String,
233    },
234
235    /// A single object was asked for and the result is not one.
236    ///
237    /// `Accept: application/vnd.pgrst.object+json` says the client will accept
238    /// exactly one row; anything else is a negotiation failure rather than a
239    /// missing resource, which is why it is 406 and not 404.
240    #[error("Cannot coerce the result to a single JSON object")]
241    NotSingular {
242        /// How many rows the query actually returned.
243        rows: usize,
244    },
245
246    /// A path with more segments than any resource has.
247    #[error("Invalid path specified in request URL")]
248    InvalidResourcePath,
249
250    /// A `PUT` whose filters do not name exactly one row by its key.
251    ///
252    /// `PUT` writes the row the URL names, so the URL has to name exactly one:
253    /// every primary key column, with `eq`, and nothing else.
254    #[error("Filters must include all and only primary key columns with 'eq' operators")]
255    InvalidPutFilters,
256
257    /// A `PUT` carrying a page.
258    #[error("limit/offset querystring parameters are not allowed for PUT")]
259    PutLimitNotAllowed,
260
261    /// A `PUT` whose body names a different row from its URL.
262    #[error("Payload values do not match URL in primary key column(s)")]
263    PutMatchingPk,
264
265    /// A function set `response.headers` to something that is not headers.
266    #[error(
267        "response.headers guc must be a JSON array composed of objects with a single key and a \
268         string value"
269    )]
270    InvalidGucHeaders,
271
272    /// A function set `response.status` to something that is not a status.
273    #[error("response.status guc must be a valid status code")]
274    InvalidGucStatus,
275
276    /// A mutation touched more rows than `Prefer: max-affected` allowed.
277    ///
278    /// The preference is a guard against a filter that turned out to match
279    /// more than the client meant; nothing is committed when it trips.
280    #[error("Query result exceeds max-affected preference constraint")]
281    MaxAffectedExceeded(i64),
282
283    /// Preferences the server does not know, sent with `handling=strict`.
284    ///
285    /// Strict handling is a request to be told rather than have them ignored.
286    #[error("Invalid preferences given with handling=strict")]
287    InvalidPreferences(Vec<String>),
288
289    // ========================================================================
290    // Schema Cache Errors
291    // ========================================================================
292    #[error("Schema cache not loaded")]
293    SchemaCacheNotLoaded,
294
295    #[error("Schema cache load failed: {0}")]
296    SchemaCacheLoadFailed(String),
297
298    // ========================================================================
299    // Database Errors (500/4xx depending on type)
300    // ========================================================================
301    #[error("Database error: {0}")]
302    Database(#[from] DatabaseError),
303
304    #[error("Connection pool error: {0}")]
305    ConnectionPool(String),
306
307    // ========================================================================
308    // Internal Errors (500)
309    // ========================================================================
310    #[error("Internal error: {0}")]
311    Internal(String),
312
313    #[error("Configuration error: {0}")]
314    Config(String),
315
316    // ========================================================================
317    // Plan Errors
318    // ========================================================================
319    #[error("Invalid plan: {0}")]
320    InvalidPlan(String),
321
322    #[error("Embedding error: {0}")]
323    EmbeddingError(String),
324}
325
326impl Error {
327    /// Get the HTTP status code for this error.
328    pub fn status_code(&self) -> StatusCode {
329        match self {
330            // 400 Bad Request
331            Self::InvalidPath(_)
332            | Self::InvalidQueryParam(_)
333            | Self::InvalidHeader(_)
334            | Self::InvalidBody(_)
335            | Self::MissingParameter(_)
336            | Self::AmbiguousRequest(_)
337            | Self::UnparsableQuery { .. }
338            | Self::UnknownColumn { .. }
339            | Self::NotAnEmbeddedResource(_)
340            | Self::RelatedOrderNotPossible { .. }
341            | Self::InvalidPreferences(_)
342            | Self::PutLimitNotAllowed
343            | Self::PutMatchingPk
344            | Self::MaxAffectedExceeded(_)
345            | Self::InvalidPlan(_)
346            | Self::EmbeddingError(_)
347            | Self::AggregatesNotAllowed
348            | Self::MissingGeometryColumn => StatusCode::BAD_REQUEST,
349
350            // The request names something real, but not which one of it.
351            Self::AmbiguousFunction { .. } | Self::AmbiguousRelationship { .. } => {
352                StatusCode::MULTIPLE_CHOICES
353            }
354
355            // 401 Unauthorized
356            Self::InvalidJwt(_)
357            | Self::NoSuitableJwtKey
358            | Self::JwtClaim(_)
359            | Self::MissingAuth => StatusCode::UNAUTHORIZED,
360
361            // 403 Forbidden
362            Self::InsufficientPermissions(_) => StatusCode::FORBIDDEN,
363
364            // 404 Not Found
365            Self::NotFound(_) | Self::TableNotFound { .. } | Self::FunctionNotFound { .. } => {
366                StatusCode::NOT_FOUND
367            }
368
369            // A relationship or a column the schema does not have is a fault
370            // in the request, not a missing resource: the resource addressed
371            // by the URL exists, and `?select=` asked it for something it
372            // cannot give. The table it names is what a 404 would be about.
373            Self::ColumnNotFound(_) | Self::RelationshipNotFound { .. } => StatusCode::BAD_REQUEST,
374
375            // 405 Method Not Allowed
376            Self::UnsupportedMethod(_) | Self::InvalidRpcMethod(_) | Self::InvalidPutFilters => {
377                StatusCode::METHOD_NOT_ALLOWED
378            }
379
380            // 406 Not Acceptable
381            Self::UnacceptableSchema { .. }
382            | Self::NotSingular { .. }
383            | Self::InvalidMediaType(_) => StatusCode::NOT_ACCEPTABLE,
384
385            Self::InvalidResourcePath => StatusCode::NOT_FOUND,
386
387            // A range the server cannot satisfy, which is what 416 is for --
388            // a negative limit asks for a window that does not exist rather
389            // than for one the server merely declines.
390            Self::InvalidRange(_) => StatusCode::RANGE_NOT_SATISFIABLE,
391
392            // 500 Internal Server Error
393            Self::JwtSecretMissing
394            | Self::RaiseNotUnderstood(_)
395            | Self::InvalidGucHeaders
396            | Self::InvalidGucStatus
397            | Self::SchemaCacheNotLoaded
398            | Self::SchemaCacheLoadFailed(_)
399            | Self::ConnectionPool(_)
400            | Self::Internal(_)
401            | Self::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
402
403            // Database errors map based on type
404            Self::Database(db_err) => db_err.status_code(),
405        }
406    }
407
408    /// Get the error code for API responses.
409    pub fn code(&self) -> &'static str {
410        match self {
411            Self::InvalidPath(_) => "PGRST100",
412            Self::AggregatesNotAllowed => "PGRST123",
413            // PGRST100 is the parse failure in PostgREST's taxonomy, covering
414            // both the path and the query string; PGRST101 is a function
415            // invoked with a method its volatility does not allow.
416            Self::InvalidQueryParam(_) | Self::UnparsableQuery { .. } => "PGRST100",
417            // Not one of PostgREST's own: a malformed header is a request
418            // that did not parse, which is what PGRST100 covers.
419            Self::InvalidHeader(_) => "PGRST100",
420            Self::InvalidBody(_) => "PGRST102",
421            Self::UnsupportedMethod(_) => "PGRST117",
422            Self::InvalidRpcMethod(_) => "PGRST101",
423            Self::UnacceptableSchema { .. } => "PGRST106",
424            Self::UnknownColumn { .. } => "PGRST204",
425            Self::InvalidRange(_) => "PGRST103",
426            Self::InvalidMediaType(_) => "PGRST107",
427            Self::MissingParameter(_) => "PGRST109",
428            Self::AmbiguousRequest(_) => "PGRST110",
429            Self::AmbiguousFunction { .. } => "PGRST203",
430            Self::AmbiguousRelationship { .. } => "PGRST201",
431
432            Self::InvalidJwt(_) | Self::NoSuitableJwtKey => "PGRST301",
433            Self::JwtSecretMissing => "PGRST300",
434            Self::JwtClaim(_) => "PGRST303",
435            Self::MissingAuth => "PGRST302",
436            Self::InsufficientPermissions(_) => "PGRST203",
437
438            Self::NotFound(_) => "PGRST205",
439            Self::TableNotFound { .. } => "PGRST205",
440            Self::FunctionNotFound { .. } => "PGRST202",
441            Self::MissingGeometryColumn => "22023",
442            Self::ColumnNotFound(_) => "PGRST204",
443            Self::RelationshipNotFound { .. } => "PGRST200",
444            Self::NotAnEmbeddedResource(_) => "PGRST108",
445            Self::NotSingular { .. } => "PGRST116",
446            Self::InvalidResourcePath => "PGRST125",
447            Self::InvalidPutFilters => "PGRST105",
448            Self::PutLimitNotAllowed => "PGRST114",
449            Self::PutMatchingPk => "PGRST115",
450            Self::MaxAffectedExceeded(_) => "PGRST124",
451            Self::RaiseNotUnderstood(_) => "PGRST121",
452            Self::InvalidGucHeaders => "PGRST111",
453            Self::InvalidGucStatus => "PGRST112",
454            Self::RelatedOrderNotPossible { .. } => "PGRST118",
455            Self::InvalidPreferences(_) => "PGRST122",
456
457            Self::SchemaCacheNotLoaded => "PGRST400",
458            Self::SchemaCacheLoadFailed(_) => "PGRST401",
459
460            Self::Database(e) => e.code(),
461            Self::ConnectionPool(_) => "PGRST500",
462
463            Self::Internal(_) => "PGRST900",
464            Self::Config(_) => "PGRST901",
465
466            Self::InvalidPlan(_) => "PGRST600",
467            Self::EmbeddingError(_) => "PGRST601",
468        }
469    }
470
471    /// Convert to JSON error response.
472    pub fn to_json(&self) -> serde_json::Value {
473        serde_json::json!({
474            "code": self.code(),
475            "message": self.to_string(),
476            "details": self.details(),
477            "hint": self.hint(),
478        })
479    }
480
481    /// Get additional details for the error.
482    pub fn details(&self) -> Option<serde_json::Value> {
483        match self {
484            Self::Database(db_err) => db_err.details.clone().map(serde_json::Value::String),
485            // A structured list rather than a sentence: each candidate is
486            // named so the client can pick one and say which.
487            Self::AmbiguousRelationship {
488                origin,
489                target,
490                candidates,
491                ..
492            } => Some(serde_json::Value::Array(
493                candidates
494                    .iter()
495                    .map(|(cardinality, description)| {
496                        serde_json::json!({
497                            "cardinality": cardinality,
498                            "embedding": format!("{} with {}", origin, target),
499                            "relationship": description,
500                        })
501                    })
502                    .collect(),
503            )),
504            Self::RelationshipNotFound {
505                origin,
506                target,
507                hint,
508                schema,
509                ..
510            } => Some(serde_json::Value::String(format!(
511                "Searched for a foreign key relationship between '{}' and '{}'{} in the schema \
512                 '{}', but no matches were found.",
513                origin,
514                target,
515                match hint {
516                    Some(hint) => format!(" using the hint '{}'", hint),
517                    None => String::new(),
518                },
519                schema
520            ))),
521            Self::RelatedOrderNotPossible { origin, relation } => {
522                Some(serde_json::Value::String(format!(
523                    "'{}' and '{}' do not form a many-to-one or one-to-one relationship",
524                    origin, relation
525                )))
526            }
527            Self::InvalidRange(reason) => Some(serde_json::Value::String(reason.clone())),
528            Self::MaxAffectedExceeded(affected) => Some(serde_json::Value::String(format!(
529                "The query affects {} rows",
530                affected
531            ))),
532            Self::NotSingular { rows } => Some(serde_json::Value::String(format!(
533                "The result contains {} rows",
534                rows
535            ))),
536            Self::InvalidPreferences(invalid) => Some(serde_json::Value::String(format!(
537                "Invalid preferences: {}",
538                invalid.join(", ")
539            ))),
540            Self::RaiseNotUnderstood(fault) => Some(serde_json::Value::String(fault.details())),
541            Self::NoSuitableJwtKey => Some(serde_json::Value::String(
542                "No suitable key was found to decode the JWT".into(),
543            )),
544            Self::UnparsableQuery { expected, .. } => {
545                Some(serde_json::Value::String(expected.clone()))
546            }
547            Self::FunctionNotFound {
548                name,
549                params,
550                single_param,
551                ..
552            } => Some(serde_json::Value::String(format!(
553                "Searched for the function {} {}, but no matches were found in the schema cache.",
554                name,
555                match single_param.as_deref() {
556                    // A body that is one value carries no argument names, so
557                    // the only thing that could have matched is the parameter
558                    // that takes it.
559                    Some(other) if other != JSON_PARAM =>
560                        format!("with a single unnamed {} parameter", other),
561                    Some(_) => format!(
562                        "{} or with a single unnamed {} parameter",
563                        describe_params(params),
564                        JSON_PARAM
565                    ),
566                    None => describe_params(params),
567                }
568            ))),
569            _ => None,
570        }
571    }
572
573    /// Get a hint for resolving the error.
574    pub fn hint(&self) -> Option<String> {
575        match self {
576            Self::NotAnEmbeddedResource(name) => Some(format!(
577                "Verify that '{}' is included in the 'select' query parameter.",
578                name
579            )),
580            Self::TableNotFound { suggestion, .. } => suggestion
581                .as_ref()
582                .map(|name| format!("Perhaps you meant the table '{}'", name)),
583            Self::RelationshipNotFound {
584                target, suggestion, ..
585            } => suggestion
586                .as_ref()
587                .map(|name| format!("Perhaps you meant '{}' instead of '{}'.", name, target)),
588            Self::AmbiguousFunction { .. } => Some(
589                "Try renaming the parameters or the function itself in the database so function \
590                 overloading can be resolved"
591                    .into(),
592            ),
593            Self::AmbiguousRelationship {
594                target,
595                disambiguated,
596                ..
597            } => Some(format!(
598                "Try changing '{}' to one of the following: {}. Find the desired relationship in \
599                 the 'details' key.",
600                target,
601                disambiguated
602                    .iter()
603                    .map(|name| format!("'{}'", name))
604                    .collect::<Vec<_>>()
605                    .join(", ")
606            )),
607            Self::RaiseNotUnderstood(fault) => Some(fault.hint().into()),
608            // A body that is one value names no arguments, so there is no
609            // near miss to point at: any function of that name would have
610            // done, and none of them takes this body.
611            Self::FunctionNotFound {
612                candidate,
613                single_param,
614                ..
615            } if !names_only_one_param(single_param.as_deref()) => candidate
616                .as_ref()
617                .map(|c| format!("Perhaps you meant to call the function {}", c)),
618            Self::FunctionNotFound { .. } => None,
619            // The schemas a server exposes are part of its contract, not an
620            // internal detail, so naming them is how the client is told what
621            // to ask for instead.
622            Self::UnacceptableSchema { exposed, .. } => Some(format!(
623                "Only the following schemas are exposed: {}",
624                exposed.join(", ")
625            )),
626            Self::Database(db_err) => db_err.hint.clone(),
627            _ => None,
628        }
629    }
630}
631
632/// How a function's argument list reads in an error message.
633///
634/// PostgREST words the no-argument case differently rather than printing an
635/// empty list, and the messages are matched verbatim by clients.
636/// How the message spells the type of a single unnamed JSON parameter.
637///
638/// It is the one media type that can also carry named arguments, so it reads
639/// as an alternative rather than as the only thing looked for.
640pub const JSON_PARAM: &str = "json/jsonb";
641
642/// Whether the request could only ever have matched one unnamed parameter.
643///
644/// True for a body that is one value -- text, xml or bytes -- which carries no
645/// argument names at all.
646pub fn names_only_one_param(single_param: Option<&str>) -> bool {
647    matches!(single_param, Some(other) if other != JSON_PARAM)
648}
649
650pub fn function_signature(name: &str, params: &[String]) -> String {
651    match params.is_empty() {
652        true => format!("{} without parameters", name),
653        false => format!("{}({})", name, params.join(", ")),
654    }
655}
656
657fn describe_params(params: &[String]) -> String {
658    match params.len() {
659        0 => "without parameters".to_string(),
660        1 => format!("with parameter {}", params[0]),
661        _ => format!("with parameters {}", params.join(", ")),
662    }
663}
664
665/// Database-specific error type.
666#[derive(Error, Debug)]
667#[error("Database error [{code}]: {message}")]
668pub struct DatabaseError {
669    pub code: String,
670    pub message: String,
671    pub details: Option<String>,
672    pub hint: Option<String>,
673    pub constraint: Option<String>,
674    pub table: Option<String>,
675    pub column: Option<String>,
676}
677
678/// What was wrong with a `RAISE sqlstate 'PGRST'`.
679#[derive(Clone, Debug)]
680pub enum RaiseFault {
681    /// The MESSAGE was not the JSON object it has to be.
682    Message(String),
683    /// The DETAIL was not the JSON object it has to be.
684    Detail(String),
685    /// There was no DETAIL, and the status has to come from somewhere.
686    NoDetail,
687}
688
689impl RaiseFault {
690    /// What the schema author has to look at.
691    pub fn details(&self) -> String {
692        match self {
693            Self::Message(raw) => format!("Invalid JSON value for MESSAGE: '{}'", raw),
694            Self::Detail(raw) => format!("Invalid JSON value for DETAIL: '{}'", raw),
695            Self::NoDetail => "DETAIL is missing in the RAISE statement".to_string(),
696        }
697    }
698
699    /// What the document should have contained.
700    pub fn hint(&self) -> &'static str {
701        match self {
702            Self::Message(_) => {
703                "MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and \
704                 optional keys: 'details', 'hint'."
705            }
706            _ => {
707                "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and \
708                 optional key: 'status_text'."
709            }
710        }
711    }
712}
713
714/// A response a database function asked for outright.
715#[derive(Clone, Debug)]
716pub struct RaisedResponse {
717    /// The HTTP status to answer with.
718    pub status: u16,
719    /// The reason phrase, where the schema named one of its own.
720    pub status_text: Option<String>,
721    /// Headers to add to the response.
722    pub headers: Vec<(String, String)>,
723    /// The response body, verbatim.
724    pub body: serde_json::Value,
725}
726
727impl DatabaseError {
728    /// The response a `RAISE sqlstate 'PGRST'` asked for.
729    ///
730    /// A function can take over the whole response by raising that SQLSTATE:
731    /// the message is the body, and the detail carries the status, an optional
732    /// status text and any headers. It is how a schema returns a 402 with its
733    /// own payload without the API layer knowing anything about billing.
734    ///
735    /// `None` when this is not that error. `Err` when it is and the schema
736    /// wrote it wrongly, which is a bug in the schema rather than a response
737    /// to half-apply.
738    pub fn raised_response(&self) -> Option<std::result::Result<RaisedResponse, RaiseFault>> {
739        if self.code != "PGRST" {
740            return None;
741        }
742        Some(self.parse_raise())
743    }
744
745    fn parse_raise(&self) -> std::result::Result<RaisedResponse, RaiseFault> {
746        let object = |raw: &str| -> Option<serde_json::Map<String, serde_json::Value>> {
747            match serde_json::from_str(raw) {
748                Ok(serde_json::Value::Object(map)) => Some(map),
749                _ => None,
750            }
751        };
752
753        // The message is the body, and a body has to say what went wrong and
754        // under what code -- the two keys a client reads first.
755        let message =
756            object(&self.message).ok_or_else(|| RaiseFault::Message(self.message.clone()))?;
757        if !message.contains_key("code") || !message.contains_key("message") {
758            return Err(RaiseFault::Message(self.message.clone()));
759        }
760
761        let raw_detail = self.details.as_deref().ok_or(RaiseFault::NoDetail)?;
762        let detail =
763            object(raw_detail).ok_or_else(|| RaiseFault::Detail(raw_detail.to_string()))?;
764        let status = detail
765            .get("status")
766            .and_then(serde_json::Value::as_u64)
767            .ok_or_else(|| RaiseFault::Detail(raw_detail.to_string()))?;
768        let headers = detail
769            .get("headers")
770            .and_then(serde_json::Value::as_object)
771            .ok_or_else(|| RaiseFault::Detail(raw_detail.to_string()))?;
772
773        // Every key the error body has, present whether the schema wrote it or
774        // not: a client reading `details` should not have to tell "no detail"
775        // from "no such key".
776        let key = |name: &str| {
777            message
778                .get(name)
779                .cloned()
780                .unwrap_or(serde_json::Value::Null)
781        };
782
783        Ok(RaisedResponse {
784            status: status as u16,
785            status_text: detail
786                .get("status_text")
787                .and_then(serde_json::Value::as_str)
788                .map(str::to_string),
789            headers: headers
790                .iter()
791                .filter_map(|(name, value)| Some((name.clone(), value.as_str()?.to_string())))
792                .collect(),
793            body: serde_json::json!({
794                "code": key("code"),
795                "message": key("message"),
796                "details": key("details"),
797                "hint": key("hint"),
798            }),
799        })
800    }
801
802    /// The HTTP status a PostgreSQL error code maps to.
803    ///
804    /// PostgREST's table (`mapSQLtoHTTP` in `Error.hs`), which is part of its
805    /// contract rather than an implementation detail: a client distinguishes a
806    /// missing function from a bad argument by the status.
807    ///
808    /// The default is 400, not 500: the great majority of SQLSTATEs a request
809    /// can provoke are provoked by the request. Reporting them as server
810    /// errors tells the client to retry something that will never succeed.
811    pub fn status_code(&self) -> StatusCode {
812        let code = self.code.as_str();
813        let message = self.message.as_str();
814
815        match self.raised_response() {
816            Some(Ok(raised)) => {
817                if let Ok(status) = StatusCode::from_u16(raised.status) {
818                    return status;
819                }
820            }
821            // A raise the schema wrote wrongly is the schema's fault, and a
822            // fault in the server's own data is a 500.
823            Some(Err(_)) => return StatusCode::INTERNAL_SERVER_ERROR,
824            None => {}
825        }
826
827        // `PT<nnn>` is a status code raised by the schema itself: a function
828        // says `RAISE sqlstate 'PT402'` to answer 402, with the message as the
829        // status text.
830        if let Some(digits) = code.strip_prefix("PT") {
831            if let Ok(status) = digits.parse::<u16>() {
832                if let Ok(status) = StatusCode::from_u16(status) {
833                    return status;
834                }
835            }
836            return StatusCode::INTERNAL_SERVER_ERROR;
837        }
838
839        match code {
840            "23503" | "23505" => StatusCode::CONFLICT,
841            // A write attempted inside the read-only transaction a read
842            // request runs in. The request was not wrong, the method was.
843            "25006" => StatusCode::METHOD_NOT_ALLOWED,
844            // Cardinality violation. `pg-safeupdate` raises it for an
845            // unqualified UPDATE or DELETE, which is the client's doing;
846            // anything else is a function or view misbehaving.
847            "21000" => match message.ends_with("requires a WHERE clause") {
848                true => StatusCode::BAD_REQUEST,
849                false => StatusCode::INTERNAL_SERVER_ERROR,
850            },
851            // Invalid parameter value, which is also how PostgreSQL reports a
852            // `SET ROLE` to a role that does not exist.
853            "22023" => match message.starts_with("role") && message.ends_with("does not exist") {
854                true => StatusCode::UNAUTHORIZED,
855                false => StatusCode::BAD_REQUEST,
856            },
857            "53400" => StatusCode::INTERNAL_SERVER_ERROR,
858            "57P01" => StatusCode::SERVICE_UNAVAILABLE,
859            "P0001" => StatusCode::BAD_REQUEST,
860            // `xmlagg` is missing only when the request asked for XML the
861            // server cannot produce, which is a negotiation failure.
862            "42883" => match message.starts_with("function xmlagg(") {
863                true => StatusCode::NOT_ACCEPTABLE,
864                false => StatusCode::NOT_FOUND,
865            },
866            "42P01" => StatusCode::NOT_FOUND,
867            "42P17" => StatusCode::INTERNAL_SERVER_ERROR,
868            // Without authentication the client may simply not have said who
869            // it is; with it, it has and is still not permitted.
870            "42501" => StatusCode::UNAUTHORIZED,
871            _ => match code.as_bytes() {
872                [b'0', b'8', ..] => StatusCode::SERVICE_UNAVAILABLE,
873                [b'0', b'9', ..] => StatusCode::INTERNAL_SERVER_ERROR,
874                [b'0', b'L', ..] | [b'0', b'P', ..] => StatusCode::FORBIDDEN,
875                [b'2', b'5', ..] => StatusCode::INTERNAL_SERVER_ERROR,
876                [b'2', b'8', ..] => StatusCode::FORBIDDEN,
877                [b'2', b'D', ..] => StatusCode::INTERNAL_SERVER_ERROR,
878                [b'3', b'8', ..] | [b'3', b'9', ..] | [b'3', b'B', ..] => {
879                    StatusCode::INTERNAL_SERVER_ERROR
880                }
881                [b'4', b'0', ..] => StatusCode::INTERNAL_SERVER_ERROR,
882                [b'5', b'3', ..] => StatusCode::SERVICE_UNAVAILABLE,
883                [b'5', b'4', ..] | [b'5', b'5', ..] | [b'5', b'7', ..] | [b'5', b'8', ..] => {
884                    StatusCode::INTERNAL_SERVER_ERROR
885                }
886                [b'F', b'0', ..] | [b'H', b'V', ..] => StatusCode::INTERNAL_SERVER_ERROR,
887                [b'P', b'0', ..] => StatusCode::INTERNAL_SERVER_ERROR,
888                [b'X', b'X', ..] => StatusCode::INTERNAL_SERVER_ERROR,
889                _ => StatusCode::BAD_REQUEST,
890            },
891        }
892    }
893
894    /// Get error code for API response.
895    pub fn code(&self) -> &'static str {
896        match self.code.as_str() {
897            c if c.starts_with("23") => "PGRST503", // Constraint violation
898            c if c.starts_with("42") => "PGRST504", // SQL error
899            c if c.starts_with("28") => "PGRST505", // Auth error
900            _ => "PGRST500",                        // Generic database error
901        }
902    }
903}
904
905#[cfg(test)]
906mod tests {
907    use super::*;
908
909    #[test]
910    fn test_error_status_codes() {
911        assert_eq!(
912            Error::InvalidQueryParam("test".into()).status_code(),
913            StatusCode::BAD_REQUEST
914        );
915        assert_eq!(Error::MissingAuth.status_code(), StatusCode::UNAUTHORIZED);
916        assert_eq!(
917            Error::TableNotFound {
918                name: "users".into(),
919                suggestion: None
920            }
921            .status_code(),
922            StatusCode::NOT_FOUND
923        );
924        assert_eq!(
925            Error::UnsupportedMethod("TRACE".into()).status_code(),
926            StatusCode::METHOD_NOT_ALLOWED
927        );
928    }
929
930    #[test]
931    fn test_error_codes() {
932        // A malformed query string is a parse failure, PGRST100 -- the same
933        // code as a malformed path. PGRST101 means something else entirely.
934        assert_eq!(Error::InvalidQueryParam("test".into()).code(), "PGRST100");
935        assert_eq!(Error::MissingAuth.code(), "PGRST302");
936        assert_eq!(
937            Error::TableNotFound {
938                name: "users".into(),
939                suggestion: None
940            }
941            .code(),
942            "PGRST205"
943        );
944    }
945
946    #[test]
947    fn test_database_error_status() {
948        let constraint_error = DatabaseError {
949            code: "23505".into(), // unique_violation
950            message: "Duplicate key".into(),
951            details: None,
952            hint: None,
953            constraint: Some("users_pkey".into()),
954            table: Some("users".into()),
955            column: None,
956        };
957        assert_eq!(constraint_error.status_code(), StatusCode::CONFLICT);
958    }
959
960    #[test]
961    fn test_error_to_json() {
962        let error = Error::InvalidQueryParam("bad filter".into());
963        let json = error.to_json();
964        assert_eq!(json["code"], "PGRST100");
965        assert!(json["message"].as_str().unwrap().contains("bad filter"));
966    }
967}