1
1
use crate :: {
2
2
db:: PoolError ,
3
3
storage:: PathNotFoundError ,
4
- web:: { cache:: CachePolicy , encode_url_path, releases:: Search , AxumErrorPage } ,
4
+ web:: { cache:: CachePolicy , encode_url_path, releases:: Search } ,
5
5
} ;
6
6
use anyhow:: anyhow;
7
7
use axum:: {
8
8
http:: StatusCode ,
9
9
response:: { IntoResponse , Response as AxumResponse } ,
10
+ Json ,
10
11
} ;
11
12
use std:: borrow:: Cow ;
13
+ use tracing:: error;
14
+
15
+ use super :: AxumErrorPage ;
12
16
13
17
#[ derive( Debug , thiserror:: Error ) ]
14
18
pub enum AxumNope {
@@ -32,90 +36,169 @@ pub enum AxumNope {
32
36
Redirect ( String , CachePolicy ) ,
33
37
}
34
38
35
- impl IntoResponse for AxumNope {
36
- fn into_response ( self ) -> AxumResponse {
39
+ // FUTURE: Ideally, the split between the 3 kinds of responses would
40
+ // be done by having multiple nested enums in the first place instead
41
+ // of just `AxumNope`, to keep everything statically type-checked
42
+ // throughout instead of having the potential for a runtime error.
43
+
44
+ impl AxumNope {
45
+ fn into_error_response ( self ) -> ErrorResponse {
37
46
match self {
38
47
AxumNope :: ResourceNotFound => {
39
48
// user tried to navigate to a resource (doc page/file) that doesn't exist
40
- AxumErrorPage {
49
+ ErrorResponse :: ErrorInfo ( ErrorInfo {
41
50
title : "The requested resource does not exist" ,
42
51
message : "no such resource" . into ( ) ,
43
52
status : StatusCode :: NOT_FOUND ,
44
- }
45
- . into_response ( )
53
+ } )
46
54
}
47
-
48
- AxumNope :: BuildNotFound => AxumErrorPage {
55
+ AxumNope :: BuildNotFound => ErrorResponse :: ErrorInfo ( ErrorInfo {
49
56
title : "The requested build does not exist" ,
50
57
message : "no such build" . into ( ) ,
51
58
status : StatusCode :: NOT_FOUND ,
52
- }
53
- . into_response ( ) ,
54
-
59
+ } ) ,
55
60
AxumNope :: CrateNotFound => {
56
61
// user tried to navigate to a crate that doesn't exist
57
62
// TODO: Display the attempted crate and a link to a search for said crate
58
- AxumErrorPage {
63
+ ErrorResponse :: ErrorInfo ( ErrorInfo {
59
64
title : "The requested crate does not exist" ,
60
65
message : "no such crate" . into ( ) ,
61
66
status : StatusCode :: NOT_FOUND ,
62
- }
63
- . into_response ( )
67
+ } )
64
68
}
65
-
66
- AxumNope :: OwnerNotFound => AxumErrorPage {
69
+ AxumNope :: OwnerNotFound => ErrorResponse :: ErrorInfo ( ErrorInfo {
67
70
title : "The requested owner does not exist" ,
68
71
message : "no such owner" . into ( ) ,
69
72
status : StatusCode :: NOT_FOUND ,
70
- }
71
- . into_response ( ) ,
72
-
73
+ } ) ,
73
74
AxumNope :: VersionNotFound => {
74
75
// user tried to navigate to a crate with a version that does not exist
75
76
// TODO: Display the attempted crate and version
76
- AxumErrorPage {
77
+ ErrorResponse :: ErrorInfo ( ErrorInfo {
77
78
title : "The requested version does not exist" ,
78
79
message : "no such version for this crate" . into ( ) ,
79
80
status : StatusCode :: NOT_FOUND ,
80
- }
81
- . into_response ( )
81
+ } )
82
82
}
83
83
AxumNope :: NoResults => {
84
84
// user did a search with no search terms
85
- Search {
85
+ ErrorResponse :: Search ( Search {
86
86
title : "No results given for empty search query" . to_owned ( ) ,
87
87
status : StatusCode :: NOT_FOUND ,
88
88
..Default :: default ( )
89
- }
90
- . into_response ( )
89
+ } )
91
90
}
92
- AxumNope :: BadRequest ( source) => AxumErrorPage {
91
+ AxumNope :: BadRequest ( source) => ErrorResponse :: ErrorInfo ( ErrorInfo {
93
92
title : "Bad request" ,
94
93
message : Cow :: Owned ( source. to_string ( ) ) ,
95
94
status : StatusCode :: BAD_REQUEST ,
96
- }
97
- . into_response ( ) ,
95
+ } ) ,
98
96
AxumNope :: InternalError ( source) => {
99
- let web_error = crate :: web:: AxumErrorPage {
97
+ crate :: utils:: report_error ( & source) ;
98
+ ErrorResponse :: ErrorInfo ( ErrorInfo {
100
99
title : "Internal Server Error" ,
101
100
message : Cow :: Owned ( source. to_string ( ) ) ,
102
101
status : StatusCode :: INTERNAL_SERVER_ERROR ,
103
- } ;
104
-
105
- crate :: utils:: report_error ( & source) ;
106
-
107
- web_error. into_response ( )
102
+ } )
108
103
}
109
104
AxumNope :: Redirect ( target, cache_policy) => {
110
105
match super :: axum_cached_redirect ( & encode_url_path ( & target) , cache_policy) {
111
- Ok ( response) => response. into_response ( ) ,
112
- Err ( err) => AxumNope :: InternalError ( err) . into_response ( ) ,
106
+ Ok ( response) => ErrorResponse :: Redirect ( response) ,
107
+ // Recurse 1 step:
108
+ Err ( err) => AxumNope :: InternalError ( err) . into_error_response ( ) ,
113
109
}
114
110
}
115
111
}
116
112
}
117
113
}
118
114
115
+ // A response representing an outcome from `AxumNope`, usable in both
116
+ // HTML or JSON (API) based endpoints.
117
+ enum ErrorResponse {
118
+ // Info representable both as HTML or as JSON
119
+ ErrorInfo ( ErrorInfo ) ,
120
+ // Redirect,
121
+ Redirect ( AxumResponse ) ,
122
+ // To recreate empty search page; only valid in HTML based
123
+ // endpoints.
124
+ Search ( Search ) ,
125
+ }
126
+
127
+ struct ErrorInfo {
128
+ // For the title of the page
129
+ pub title : & ' static str ,
130
+ // The error message, displayed as a description
131
+ pub message : Cow < ' static , str > ,
132
+ // The status code of the response
133
+ pub status : StatusCode ,
134
+ }
135
+
136
+ impl ErrorResponse {
137
+ fn into_html_response ( self ) -> AxumResponse {
138
+ match self {
139
+ ErrorResponse :: ErrorInfo ( ErrorInfo {
140
+ title,
141
+ message,
142
+ status,
143
+ } ) => AxumErrorPage {
144
+ title,
145
+ message,
146
+ status,
147
+ }
148
+ . into_response ( ) ,
149
+ ErrorResponse :: Redirect ( response) => response,
150
+ ErrorResponse :: Search ( search) => search. into_response ( ) ,
151
+ }
152
+ }
153
+
154
+ fn into_json_response ( self ) -> AxumResponse {
155
+ match self {
156
+ ErrorResponse :: ErrorInfo ( ErrorInfo {
157
+ title,
158
+ message,
159
+ status,
160
+ } ) => (
161
+ status,
162
+ Json ( serde_json:: json!( {
163
+ "title" : title,
164
+ "message" : message,
165
+ } ) ) ,
166
+ )
167
+ . into_response ( ) ,
168
+ ErrorResponse :: Redirect ( response) => response,
169
+ ErrorResponse :: Search ( search) => {
170
+ // FUTURE: this runtime error is avoidable by
171
+ // splitting `enum AxumNope` into hierarchical parts,
172
+ // see above.
173
+ error ! (
174
+ "expecting that handlers that return JSON error responses \
175
+ don't return Search, but got: {search:?}"
176
+ ) ;
177
+ AxumNope :: InternalError ( anyhow ! (
178
+ "bug: search HTML page returned from endpoint that returns JSON"
179
+ ) )
180
+ . into_error_response ( )
181
+ . into_json_response ( )
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ impl IntoResponse for AxumNope {
188
+ fn into_response ( self ) -> AxumResponse {
189
+ self . into_error_response ( ) . into_html_response ( )
190
+ }
191
+ }
192
+
193
+ /// `AxumNope` but generating error responses in JSON (for API).
194
+ pub ( crate ) struct JsonAxumNope ( pub AxumNope ) ;
195
+
196
+ impl IntoResponse for JsonAxumNope {
197
+ fn into_response ( self ) -> AxumResponse {
198
+ self . 0 . into_error_response ( ) . into_json_response ( )
199
+ }
200
+ }
201
+
119
202
impl From < anyhow:: Error > for AxumNope {
120
203
fn from ( err : anyhow:: Error ) -> Self {
121
204
match err. downcast :: < AxumNope > ( ) {
@@ -141,6 +224,7 @@ impl From<PoolError> for AxumNope {
141
224
}
142
225
143
226
pub ( crate ) type AxumResult < T > = Result < T , AxumNope > ;
227
+ pub ( crate ) type JsonAxumResult < T > = Result < T , JsonAxumNope > ;
144
228
145
229
#[ cfg( test) ]
146
230
mod tests {
0 commit comments