gpui/http_client.rs
1use futures::future::BoxFuture;
2use http::StatusCode;
3
4/// An HTTP status code and a fully buffered response body.
5///
6/// The body is owned bytes, so a caller can decode an image or inspect an error response
7/// after the request has finished. This interface does not expose response headers or a stream.
8pub struct HttpResponse {
9 /// The status returned by the server, including error statuses such as `404 Not Found`.
10 pub status: StatusCode,
11 /// The complete response body, read before the response is returned.
12 pub body: Vec<u8>,
13}
14
15/// The transport GPUI uses for resources such as image URLs.
16///
17/// Install an implementation with [`crate::Application::with_http_client`]; code inside the
18/// app retrieves it with [`crate::App::http_client`]. A newly created app uses
19/// [`NullHttpClient`], so URL-backed loads fail until a client is installed. The web
20/// constructors in `gpui_platform` install a fetch-based client; native applications must
21/// provide one before loading URL-backed assets.
22///
23/// For tests, [`BlockedHttpClient`] rejects network access. With the `test-support` feature,
24/// `FakeHttpClient` supplies a fixed status without making a request. The
25/// [images guide](crate::_documentation::guides::images_and_assets) explains loading,
26/// failure placeholders and cache eviction for a retry.
27pub trait HttpClient: 'static + Send + Sync {
28 /// Sends a GET request to `url` and returns its status and complete body.
29 ///
30 /// Follow redirects when `follow_redirects` is `true`. Return `Err` for a transport or
31 /// body-read failure. Receiving an HTTP error status, such as 404, is still an `Ok`
32 /// response: callers inspect [`HttpResponse::status`] and decide whether to use the body.
33 fn get(
34 &self,
35 url: &str,
36 follow_redirects: bool,
37 ) -> BoxFuture<'static, anyhow::Result<HttpResponse>>;
38}
39
40/// An [`HttpClient`] that fails every request because no client was configured.
41///
42/// A new app uses it until you install another client. The error text is `No HttpClient available`.
43pub struct NullHttpClient;
44
45impl HttpClient for NullHttpClient {
46 fn get(
47 &self,
48 _url: &str,
49 _follow_redirects: bool,
50 ) -> BoxFuture<'static, anyhow::Result<HttpResponse>> {
51 Box::pin(async { anyhow::bail!("No HttpClient available") })
52 }
53}
54
55/// An [`HttpClient`] that refuses every request with an I/O `PermissionDenied` error.
56///
57/// Install it when a run must not reach the network, such as a hermetic test.
58pub struct BlockedHttpClient;
59
60impl BlockedHttpClient {
61 /// Creates a client that refuses every request. This is the same value as
62 /// [`Default::default`].
63 pub fn new() -> Self {
64 Self
65 }
66}
67
68impl Default for BlockedHttpClient {
69 fn default() -> Self {
70 Self
71 }
72}
73
74impl HttpClient for BlockedHttpClient {
75 fn get(
76 &self,
77 _url: &str,
78 _follow_redirects: bool,
79 ) -> BoxFuture<'static, anyhow::Result<HttpResponse>> {
80 Box::pin(async {
81 Err(std::io::Error::new(
82 std::io::ErrorKind::PermissionDenied,
83 "BlockedHttpClient disallowed request",
84 )
85 .into())
86 })
87 }
88}
89
90/// An [`HttpClient`] for tests that answers every request with one status and an empty body.
91///
92/// Available in unit tests and with the `test-support` feature. It never performs network I/O.
93#[cfg(any(test, feature = "test-support"))]
94pub struct FakeHttpClient {
95 status: StatusCode,
96}
97
98#[cfg(any(test, feature = "test-support"))]
99impl FakeHttpClient {
100 /// Returns a client that answers every request with `404 Not Found`.
101 pub fn with_404_response() -> std::sync::Arc<dyn HttpClient> {
102 std::sync::Arc::new(Self {
103 status: StatusCode::NOT_FOUND,
104 })
105 }
106
107 /// Returns a client that answers every request with `200 OK`.
108 pub fn with_200_response() -> std::sync::Arc<dyn HttpClient> {
109 std::sync::Arc::new(Self {
110 status: StatusCode::OK,
111 })
112 }
113}
114
115#[cfg(any(test, feature = "test-support"))]
116impl HttpClient for FakeHttpClient {
117 fn get(
118 &self,
119 _url: &str,
120 _follow_redirects: bool,
121 ) -> BoxFuture<'static, anyhow::Result<HttpResponse>> {
122 let status = self.status;
123 Box::pin(async move {
124 Ok(HttpResponse {
125 status,
126 body: Vec::new(),
127 })
128 })
129 }
130}