Skip to main content

postrust_response/
lib.rs

1//! Response formatting for Postrust.
2//!
3//! Handles content negotiation and response formatting for JSON, CSV, and other formats.
4
5#![warn(missing_docs)]
6
7mod headers;
8pub use headers::parse_guc_headers;
9mod json;
10
11pub use headers::ContentRange;
12// Re-exported for the crates.io consumers the name is public API for. Allowed
13// here so that the re-export itself does not trip the deprecation it carries.
14#[allow(deprecated)]
15pub use headers::build_response_headers;
16pub use json::format_json_response;
17
18use http::{HeaderMap, HeaderValue, StatusCode};
19use postrust_core::{ApiRequest, MediaType};
20use serde::Serialize;
21
22/// A formatted HTTP response.
23#[derive(Clone, Debug)]
24pub struct Response {
25    /// HTTP status code
26    pub status: StatusCode,
27    /// Response headers
28    pub headers: HeaderMap,
29    /// Response body
30    pub body: bytes::Bytes,
31}
32
33impl Response {
34    /// Create a new response.
35    pub fn new(status: StatusCode, body: impl Into<bytes::Bytes>) -> Self {
36        Self {
37            status,
38            headers: HeaderMap::new(),
39            body: body.into(),
40        }
41    }
42
43    /// Create a JSON response.
44    pub fn json<T: Serialize>(status: StatusCode, value: &T) -> Result<Self, serde_json::Error> {
45        let body = serde_json::to_vec(value)?;
46        let mut response = Self::new(status, body);
47        response.set_content_type("application/json; charset=utf-8");
48        Ok(response)
49    }
50
51    /// Create an empty response.
52    pub fn empty(status: StatusCode) -> Self {
53        Self::new(status, bytes::Bytes::new())
54    }
55
56    /// Set a header.
57    /// Add a header without replacing one of the same name.
58    ///
59    /// A response may legitimately carry two `Set-Cookie`s, which `set_header`
60    /// -- being a replace -- cannot express.
61    pub fn append_header(&mut self, name: &str, value: &str) {
62        if let (Ok(name), Ok(value)) = (
63            http::header::HeaderName::from_bytes(name.as_bytes()),
64            HeaderValue::from_str(value),
65        ) {
66            self.headers.append(name, value);
67        }
68    }
69
70    /// Set a header, replacing any already present under that name.
71    ///
72    /// A name or value that is not valid as an HTTP header is dropped rather
73    /// than reported: these are built from database output, and one bad GUC
74    /// should not fail an otherwise good response.
75    pub fn set_header(&mut self, name: &str, value: &str) {
76        if let Ok(v) = HeaderValue::from_str(value) {
77            self.headers.insert(
78                http::header::HeaderName::from_bytes(name.as_bytes()).unwrap(),
79                v,
80            );
81        }
82    }
83
84    /// Set Content-Type header.
85    pub fn set_content_type(&mut self, content_type: &str) {
86        self.set_header("content-type", content_type);
87    }
88
89    /// Set Content-Range header.
90    pub fn set_content_range(&mut self, range: &ContentRange) {
91        self.set_header("content-range", &range.to_string());
92    }
93
94    /// Set Location header.
95    pub fn set_location(&mut self, location: &str) {
96        self.set_header("location", location);
97    }
98}
99
100/// Format a query result as a response.
101pub fn format_response(
102    request: &ApiRequest,
103    result: &QueryResult,
104) -> Result<Response, FormatError> {
105    let media_type = request
106        .accept_media_types
107        .first()
108        .cloned()
109        .unwrap_or(MediaType::ApplicationJson);
110
111    // A mutation the caller wanted no representation from sends no body at
112    // all -- not an empty JSON array, which is what an empty row set would
113    // otherwise render as. The headers still apply.
114    // A media type the schema renders itself: the database produced the whole
115    // payload, so there is nothing here to serialise.
116    if let Some((media_type, body)) = &result.raw_body {
117        let mut response = Response::new(result.status, body.clone());
118        // Every media type the server names is UTF-8 except the ones that are
119        // not text at all: a stream of bytes has no encoding, and one the
120        // schema named itself is not this side's to characterise.
121        let charset = match media_type.as_str() {
122            "application/octet-stream" => "",
123            already if already.contains("charset=") => "",
124            text if text.starts_with("text/") || text.contains("json") => "; charset=utf-8",
125            _ => "",
126        };
127        response.set_content_type(&format!("{}{}", media_type, charset));
128        // The body being rendered by something other than this side does not
129        // change what else the response has to say: where the created row is,
130        // which preference was honoured, what the function set. Only the body
131        // was somebody else's to write.
132        add_common_headers(&mut response, request, result);
133        return Ok(response);
134    }
135
136    if result.omit_body {
137        // Asking for a single object is a constraint on the statement, not on
138        // the payload: a caller that named one row and changed five has not
139        // had its request answered, however little of it it wanted back.
140        if let MediaType::SingularJson { nullable, .. } = &media_type {
141            match (result.affected, nullable) {
142                (1, _) | (0, true) => {}
143                (0, false) => return Err(FormatError::NotFound),
144                _ => return Err(FormatError::MultipleRows),
145            }
146        }
147        let mut response = Response::new(result.status, bytes::Bytes::new());
148        add_common_headers(&mut response, request, result);
149        return Ok(response);
150    }
151
152    // `;nulls=stripped` asks for keys with a null value to be left out
153    // entirely, rather than sent as nulls.
154    let rows = match &media_type {
155        MediaType::SingularJson {
156            strip_nulls: true, ..
157        }
158        | MediaType::ArrayJson {
159            strip_nulls: true, ..
160        } => result.rows.iter().cloned().map(strip_nulls).collect(),
161        _ => result.rows.clone(),
162    };
163    let result = &QueryResult {
164        rows,
165        ..result.clone()
166    };
167
168    match &media_type {
169        MediaType::ApplicationJson => {
170            let body = if result.singular {
171                format_singular_or_null(&result.rows)?
172            } else {
173                format_json_response(&result.rows)?
174            };
175            let mut response = Response::new(result.status, body);
176            response.set_content_type("application/json; charset=utf-8");
177            add_common_headers(&mut response, request, result);
178            Ok(response)
179        }
180        MediaType::TextCsv => {
181            // CSV formatting would go here
182            let body = format_csv_response(&result.rows)?;
183            let mut response = Response::new(result.status, body);
184            response.set_content_type("text/csv; charset=utf-8");
185            add_common_headers(&mut response, request, result);
186            Ok(response)
187        }
188        MediaType::SingularJson { nullable, .. } => {
189            let body = format_singular_json(&result.rows, *nullable)?;
190            let mut response = Response::new(result.status, body);
191            response.set_content_type("application/vnd.pgrst.object+json; charset=utf-8");
192            add_common_headers(&mut response, request, result);
193            Ok(response)
194        }
195        other => {
196            // Default to JSON (covers `*/*`, e.g. a default curl request).
197            let body = if result.singular {
198                format_singular_or_null(&result.rows)?
199            } else {
200                format_json_response(&result.rows)?
201            };
202            let mut response = Response::new(result.status, body);
203            // The body is JSON either way, but a client that asked for one of
204            // PostgREST's own JSON media types is told it got that: the type
205            // is how it knows the shape it negotiated was honoured.
206            let content_type = match other {
207                MediaType::ArrayJson { .. } => "application/vnd.pgrst.array+json; charset=utf-8",
208                MediaType::OpenApi => "application/openapi+json; charset=utf-8",
209                _ => "application/json; charset=utf-8",
210            };
211            response.set_content_type(content_type);
212            add_common_headers(&mut response, request, result);
213            Ok(response)
214        }
215    }
216}
217
218/// Which preferences this request could honour.
219pub(crate) fn preference_scope(
220    request: &ApiRequest,
221) -> postrust_core::api_request::preferences::PreferenceScope {
222    use postrust_core::api_request::preferences::PreferenceScope;
223    use postrust_core::api_request::{Action, DbAction, Mutation};
224
225    let Action::Db(action) = &request.action else {
226        return PreferenceScope::read();
227    };
228
229    match action {
230        DbAction::RelationMut { mutation, .. } => PreferenceScope {
231            resolution: matches!(mutation, Mutation::Create),
232            representation: true,
233            missing: matches!(mutation, Mutation::Create | Mutation::Update),
234            max_affected: matches!(mutation, Mutation::Update | Mutation::Delete),
235        },
236        DbAction::Routine { .. } => PreferenceScope {
237            max_affected: true,
238            ..PreferenceScope::read()
239        },
240        _ => PreferenceScope::read(),
241    }
242}
243
244/// Remove every key whose value is null, at every depth.
245///
246/// `Accept: application/vnd.pgrst.array+json;nulls=stripped` asks for a body
247/// carrying only what a row actually has, which for a wide table with few
248/// populated columns is most of the response.
249fn strip_nulls(value: serde_json::Value) -> serde_json::Value {
250    match value {
251        serde_json::Value::Object(fields) => serde_json::Value::Object(
252            fields
253                .into_iter()
254                .filter(|(_, value)| !value.is_null())
255                .map(|(key, value)| (key, strip_nulls(value)))
256                .collect(),
257        ),
258        serde_json::Value::Array(items) => {
259            serde_json::Value::Array(items.into_iter().map(strip_nulls).collect())
260        }
261        other => other,
262    }
263}
264
265/// Add common response headers.
266fn add_common_headers(response: &mut Response, request: &ApiRequest, result: &QueryResult) {
267    // A function's own headers, added rather than replaced: the shape of the
268    // setting is an array precisely so that a name may repeat.
269    if let Some(guc) = &result.guc_headers {
270        if let Some(headers) = crate::headers::parse_guc_headers(guc) {
271            for (name, value) in headers {
272                response.append_header(&name, &value);
273            }
274        }
275    }
276
277    // Content-Range
278    if let Some(range) = &result.content_range {
279        response.set_content_range(range);
280    }
281
282    // Location (for POST)
283    if let Some(location) = &result.location {
284        response.set_location(location);
285    }
286
287    // Allow (for OPTIONS)
288    if let Some(allow) = &result.allow {
289        response.set_header("allow", allow);
290    }
291
292    // Preference-Applied
293    if let Some(applied) = postrust_core::api_request::preferences::preference_applied(
294        &request.preferences,
295        preference_scope(request),
296    ) {
297        response.set_header("preference-applied", &applied);
298    }
299
300    // Content-Profile
301    if request.negotiated_by_profile {
302        response.set_header("content-profile", &request.schema);
303    }
304}
305
306/// Format a result as a single value (PostgREST-compatibility RPC responses).
307///
308/// A single row is emitted bare (not array-wrapped), no rows becomes `null`,
309/// and the (unexpected) multi-row case falls back to a JSON array so no data
310/// is silently dropped.
311fn format_singular_or_null(rows: &[serde_json::Value]) -> Result<bytes::Bytes, FormatError> {
312    match rows.len() {
313        0 => Ok(bytes::Bytes::from_static(b"null")),
314        1 => Ok(bytes::Bytes::from(serde_json::to_vec(&rows[0])?)),
315        _ => format_json_response(rows),
316    }
317}
318
319/// Format singular JSON (single object or null).
320fn format_singular_json(
321    rows: &[serde_json::Value],
322    nullable: bool,
323) -> Result<bytes::Bytes, FormatError> {
324    match rows.len() {
325        0 if nullable => Ok(bytes::Bytes::from_static(b"null")),
326        0 => Err(FormatError::NotFound),
327        1 => Ok(bytes::Bytes::from(serde_json::to_vec(&rows[0])?)),
328        _ => Err(FormatError::MultipleRows),
329    }
330}
331
332/// Format CSV response.
333fn format_csv_response(rows: &[serde_json::Value]) -> Result<bytes::Bytes, FormatError> {
334    if rows.is_empty() {
335        return Ok(bytes::Bytes::new());
336    }
337
338    let mut output = Vec::new();
339
340    // Get headers from first row
341    if let Some(serde_json::Value::Object(map)) = rows.first() {
342        let headers: Vec<&str> = map.keys().map(|s| s.as_str()).collect();
343        output.extend_from_slice(headers.join(",").as_bytes());
344        output.push(b'\n');
345
346        // Write rows
347        for row in rows {
348            if let serde_json::Value::Object(row_map) = row {
349                let values: Vec<String> = headers
350                    .iter()
351                    .map(|h| row_map.get(*h).map(csv_escape).unwrap_or_default())
352                    .collect();
353                output.extend_from_slice(values.join(",").as_bytes());
354                output.push(b'\n');
355            }
356        }
357    }
358
359    Ok(bytes::Bytes::from(output))
360}
361
362/// Escape a value for CSV.
363fn csv_escape(value: &serde_json::Value) -> String {
364    match value {
365        serde_json::Value::String(s) => {
366            if s.contains(',') || s.contains('"') || s.contains('\n') {
367                format!("\"{}\"", s.replace('"', "\"\""))
368            } else {
369                s.clone()
370            }
371        }
372        serde_json::Value::Null => String::new(),
373        other => other.to_string(),
374    }
375}
376
377/// Query result for response formatting.
378#[derive(Clone, Debug, Default)]
379pub struct QueryResult {
380    /// HTTP status code
381    pub status: StatusCode,
382    /// Result rows
383    pub rows: Vec<serde_json::Value>,
384    /// Total row count (for pagination)
385    pub total_count: Option<i64>,
386    /// Content range
387    pub content_range: Option<ContentRange>,
388    /// Location header (for POST)
389    pub location: Option<String>,
390    /// The methods an OPTIONS request is answering about.
391    ///
392    /// Only an OPTIONS sets this; every other response leaves it `None` and
393    /// sends no `Allow`, since the question was not asked.
394    pub allow: Option<String>,
395    /// Custom headers from GUC
396    pub guc_headers: Option<String>,
397    /// Custom status from GUC
398    pub guc_status: Option<String>,
399    /// Whether to send no body at all.
400    ///
401    /// Set for a mutation the caller asked no representation from: the
402    /// response carries headers and a status but no payload, where an empty
403    /// row set would otherwise render as `[]`.
404    pub omit_body: bool,
405    /// How many rows the statement affected.
406    ///
407    /// Distinct from `rows.len()`, which is empty when no representation was
408    /// asked for. How many rows a write touched is a fact about the write, and
409    /// a caller that asked for exactly one object is owed an answer about it
410    /// whether or not it wanted the object back.
411    pub affected: usize,
412    /// Whether the result should be rendered as a single (un-arrayed) value.
413    ///
414    /// Set for PostgREST-compatibility RPC responses where the underlying
415    /// function is not set-returning: the bare object/scalar is returned
416    /// instead of a one-element array.
417    pub singular: bool,
418    /// A body the database rendered in full, with the media type it is in.
419    ///
420    /// Set when the request asked for a media type the schema declares its own
421    /// renderer for. The value is the whole payload, so it replaces the usual
422    /// JSON rendering rather than being wrapped in it.
423    pub raw_body: Option<(String, Vec<u8>)>,
424}
425
426/// Response formatting error.
427#[derive(Debug, thiserror::Error)]
428pub enum FormatError {
429    /// The rows could not be serialised. Reported as 500: the request was
430    /// answerable and this server failed to render the answer.
431    #[error("JSON serialization error: {0}")]
432    Json(#[from] serde_json::Error),
433
434    /// A singular response was asked for and no row matched.
435    #[error("Resource not found")]
436    NotFound,
437
438    /// A singular response was asked for and more than one row matched, which
439    /// is 406 rather than 500: the rows are fine, the requested shape is not.
440    #[error("Multiple rows returned for singular response")]
441    MultipleRows,
442}
443
444impl FormatError {
445    /// The HTTP status this failure is answered with.
446    pub fn status_code(&self) -> StatusCode {
447        match self {
448            Self::Json(_) => StatusCode::INTERNAL_SERVER_ERROR,
449            Self::NotFound => StatusCode::NOT_FOUND,
450            Self::MultipleRows => StatusCode::NOT_ACCEPTABLE,
451        }
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use serde_json::json;
459
460    fn result(rows: Vec<serde_json::Value>, singular: bool) -> QueryResult {
461        QueryResult {
462            status: StatusCode::OK,
463            rows,
464            singular,
465            ..Default::default()
466        }
467    }
468
469    #[test]
470    fn singular_result_renders_bare_object() {
471        let req = ApiRequest::default();
472        let resp = format_response(&req, &result(vec![json!({"ok": true})], true)).unwrap();
473        assert_eq!(&resp.body[..], br#"{"ok":true}"#);
474    }
475
476    #[test]
477    fn singular_empty_result_renders_null() {
478        let req = ApiRequest::default();
479        let resp = format_response(&req, &result(vec![], true)).unwrap();
480        assert_eq!(&resp.body[..], b"null");
481    }
482
483    #[test]
484    fn non_singular_result_renders_array() {
485        let req = ApiRequest::default();
486        let resp = format_response(&req, &result(vec![json!(1), json!(2)], false)).unwrap();
487        assert_eq!(&resp.body[..], b"[1,2]");
488    }
489}