1use http::{HeaderName, HeaderValue, Response, StatusCode, header};
14use mime::Mime;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Unsupported {
26 Streaming {
31 header: HeaderName,
33 value: HeaderValue,
35 },
36 Compression {
47 encoding: HeaderValue,
49 },
50 SetCookie {
55 name: String,
57 },
58 Upgrade,
63}
64
65impl core::fmt::Display for Unsupported {
66 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
67 match self {
68 Unsupported::Streaming { header, value } => write!(
69 f,
70 "the response streams (`{header}: {}`), and a custom protocol delivers one \
71 buffered body",
72 lossy(value)
73 ),
74 Unsupported::Compression { encoding } => write!(
75 f,
76 "the response is `{}`-encoded, and the webview will not decode what a custom \
77 protocol hands it",
78 lossy(encoding)
79 ),
80 Unsupported::SetCookie { name } => write!(
81 f,
82 "the response sets the cookie `{name}`, and the webview discards cookies from a \
83 custom protocol"
84 ),
85 Unsupported::Upgrade => f.write_str(
86 "the response asks to change protocols, and there is no connection here",
87 ),
88 }
89 }
90}
91
92impl core::error::Error for Unsupported {}
93
94#[must_use]
100pub fn unsupported<B>(response: &Response<B>) -> Option<Unsupported> {
101 let headers = response.headers();
102
103 if let Some(value) = headers.get(header::TRANSFER_ENCODING) {
104 return Some(Unsupported::Streaming {
105 header: header::TRANSFER_ENCODING,
106 value: value.clone(),
107 });
108 }
109 if let Some(value) = headers.get(header::CONTENT_TYPE)
111 && media_type(value).is_some_and(|media_type| {
112 media_type.type_() == mime::TEXT && media_type.subtype() == mime::EVENT_STREAM
113 })
114 {
115 return Some(Unsupported::Streaming {
116 header: header::CONTENT_TYPE,
117 value: value.clone(),
118 });
119 }
120
121 if let Some(value) = headers.get(header::CONTENT_ENCODING) {
122 return Some(Unsupported::Compression {
123 encoding: value.clone(),
124 });
125 }
126
127 if let Some(value) = headers.get(header::SET_COOKIE) {
128 return Some(Unsupported::SetCookie { name: name(value) });
129 }
130
131 if response.status() == StatusCode::SWITCHING_PROTOCOLS || headers.contains_key(header::UPGRADE)
132 {
133 return Some(Unsupported::Upgrade);
134 }
135
136 None
137}
138
139fn media_type(value: &HeaderValue) -> Option<Mime> {
145 value.to_str().ok()?.trim().parse().ok()
146}
147
148fn name(value: &HeaderValue) -> String {
150 let value = lossy(value);
151 match value.split_once('=') {
152 Some((name, _)) => name.trim().to_owned(),
153 None => value,
154 }
155}
156
157fn lossy(value: &HeaderValue) -> String {
160 String::from_utf8_lossy(value.as_bytes()).into_owned()
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 fn response(headers: &[(HeaderName, &str)]) -> Response<()> {
169 with_status(StatusCode::OK, headers)
170 }
171
172 fn with_status(status: StatusCode, headers: &[(HeaderName, &str)]) -> Response<()> {
173 let mut builder = Response::builder().status(status);
174 for (name, value) in headers {
175 builder = builder.header(name, *value);
176 }
177 builder.body(()).expect("a valid response")
178 }
179
180 #[test]
181 fn an_ordinary_response_is_supported() {
182 let response = response(&[
183 (header::CONTENT_TYPE, "text/html; charset=utf-8"),
184 (header::CONTENT_LENGTH, "42"),
185 (header::CACHE_CONTROL, "no-store"),
186 ]);
187 assert_eq!(unsupported(&response), None);
188 }
189
190 #[test]
191 fn chunked_transfer_is_streaming() {
192 let response = response(&[(header::TRANSFER_ENCODING, "chunked")]);
193 assert_eq!(
194 unsupported(&response),
195 Some(Unsupported::Streaming {
196 header: header::TRANSFER_ENCODING,
197 value: HeaderValue::from_static("chunked"),
198 })
199 );
200 }
201
202 #[test]
203 fn server_sent_events_are_streaming() {
204 let plain = response(&[(header::CONTENT_TYPE, "text/event-stream")]);
205 assert_eq!(
206 unsupported(&plain),
207 Some(Unsupported::Streaming {
208 header: header::CONTENT_TYPE,
209 value: HeaderValue::from_static("text/event-stream"),
210 })
211 );
212 let parameterized =
214 response(&[(header::CONTENT_TYPE, " text/event-stream; charset=utf-8")]);
215 assert!(matches!(
216 unsupported(¶meterized),
217 Some(Unsupported::Streaming { .. })
218 ));
219 let shouted = response(&[(header::CONTENT_TYPE, "TEXT/EVENT-STREAM")]);
221 assert!(matches!(
222 unsupported(&shouted),
223 Some(Unsupported::Streaming { .. })
224 ));
225 }
226
227 #[test]
228 fn a_content_type_that_merely_starts_alike_is_supported() {
229 let response = response(&[(header::CONTENT_TYPE, "text/event-streamlined")]);
230 assert_eq!(unsupported(&response), None);
231 }
232
233 #[test]
234 fn a_content_encoding_is_compression() {
235 for encoding in ["gzip", "br", "zstd", "deflate"] {
236 let response = response(&[(header::CONTENT_ENCODING, encoding)]);
237 assert_eq!(
238 unsupported(&response),
239 Some(Unsupported::Compression {
240 encoding: HeaderValue::from_str(encoding).expect("a valid header value"),
241 })
242 );
243 }
244 }
245
246 #[test]
247 fn a_set_cookie_is_named_by_its_cookie() {
248 let response = response(&[(
249 header::SET_COOKIE,
250 "__Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax",
251 )]);
252 assert_eq!(
253 unsupported(&response),
254 Some(Unsupported::SetCookie {
255 name: "__Host-session".to_owned()
256 })
257 );
258 }
259
260 #[test]
261 fn a_set_cookie_without_a_pair_still_reports() {
262 let response = response(&[(header::SET_COOKIE, "malformed")]);
263 assert_eq!(
264 unsupported(&response),
265 Some(Unsupported::SetCookie {
266 name: "malformed".to_owned()
267 })
268 );
269 }
270
271 #[test]
272 fn switching_protocols_is_an_upgrade() {
273 let response = with_status(StatusCode::SWITCHING_PROTOCOLS, &[]);
274 assert_eq!(unsupported(&response), Some(Unsupported::Upgrade));
275 }
276
277 #[test]
278 fn an_upgrade_header_is_an_upgrade() {
279 let response = response(&[(header::UPGRADE, "websocket")]);
280 assert_eq!(unsupported(&response), Some(Unsupported::Upgrade));
281 }
282
283 #[test]
284 fn the_most_structural_problem_is_reported_first() {
285 let response = response(&[
286 (header::CONTENT_TYPE, "text/event-stream"),
287 (header::CONTENT_ENCODING, "gzip"),
288 (header::SET_COOKIE, "a=b"),
289 ]);
290 assert!(matches!(
291 unsupported(&response),
292 Some(Unsupported::Streaming { .. })
293 ));
294 }
295
296 #[test]
297 fn a_redirect_is_not_unsupported_here() {
298 let response = with_status(StatusCode::SEE_OTHER, &[(header::LOCATION, "/done")]);
299 assert_eq!(unsupported(&response), None);
300 }
301}