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