@@ -83,6 +83,9 @@ fn pattern(start: usize, len: usize) -> Bytes {
8383}
8484
8585async fn handle_request ( req : Request < Incoming > ) -> Result < Response < BoxBody > , GenericError > {
86+ if req. uri ( ) . path ( ) . starts_with ( "/redirect/" ) {
87+ return handle_redirect ( req) . await ;
88+ }
8689 let response = match ( req. method ( ) , req. uri ( ) . path ( ) ) {
8790 ( & Method :: GET , "/utf8test" ) => {
8891 // UTF-8 encoded "Hello utf8"
@@ -169,6 +172,136 @@ async fn handle_request(req: Request<Incoming>) -> Result<Response<BoxBody>, Gen
169172 Ok ( response)
170173}
171174
175+ /// Redirects intentionally keep their bodies unfinished. Following a redirect
176+ /// must close that body and send the next request without collecting it.
177+ fn redirect ( status : u16 , locations : & [ String ] ) -> Result < Response < BoxBody > , GenericError > {
178+ let mut response = Response :: builder ( ) . status ( status) ;
179+ for location in locations {
180+ response = response. header ( "Location" , location) ;
181+ }
182+ Ok ( response. body ( parts ( [
183+ Part :: Data ( Bytes :: from_static ( b"discard this intermediate body" ) ) ,
184+ Part :: Pause ( Duration :: from_secs ( 60 ) ) ,
185+ ] ) ) ?)
186+ }
187+
188+ async fn handle_redirect ( req : Request < Incoming > ) -> Result < Response < BoxBody > , GenericError > {
189+ let path = req. uri ( ) . path ( ) . to_owned ( ) ;
190+ if path == "/redirect/inspect" {
191+ let method = req. method ( ) . as_str ( ) . to_owned ( ) ;
192+ let uri = req. uri ( ) . to_string ( ) ;
193+ let fields = [
194+ "host" ,
195+ "authorization" ,
196+ "cookie" ,
197+ "proxy-authorization" ,
198+ "content-type" ,
199+ "content-length" ,
200+ "x-hop" ,
201+ "x-public" ,
202+ "expect" ,
203+ "digest" ,
204+ ] ;
205+ let headers: String = fields
206+ . into_iter ( )
207+ . map ( |name| {
208+ format ! (
209+ "{name}: {}\n " ,
210+ req. headers( )
211+ . get( name)
212+ . map_or( "<absent>" , |v| v. to_str( ) . unwrap_or( "<invalid>" ) )
213+ )
214+ } )
215+ . collect ( ) ;
216+ let body = req. into_body ( ) . collect ( ) . await ?. to_bytes ( ) ;
217+ let report = format ! (
218+ "method: {method}\n uri: {uri}\n body: {}\n {headers}" ,
219+ String :: from_utf8_lossy( & body)
220+ ) ;
221+ return Ok ( Response :: builder ( )
222+ . header ( "X-Request-Method" , method)
223+ . body ( full ( report) ) ?) ;
224+ }
225+ if path == "/redirect/final" {
226+ return Ok ( Response :: new ( full ( "final" ) ) ) ;
227+ }
228+ if let Some ( left) = path. strip_prefix ( "/redirect/delayed/" ) {
229+ tokio:: time:: sleep ( Duration :: from_millis ( 250 ) ) . await ;
230+ let left: u8 = left. parse ( ) ?;
231+ return if left == 0 {
232+ Ok ( Response :: new ( parts ( [
233+ Part :: Pause ( Duration :: from_millis ( 250 ) ) ,
234+ Part :: Data ( Bytes :: from_static ( b"final" ) ) ,
235+ ] ) ) )
236+ } else {
237+ redirect ( 302 , & [ format ! ( "/redirect/delayed/{}" , left - 1 ) ] )
238+ } ;
239+ }
240+ if let Some ( left) = path. strip_prefix ( "/redirect/chain/" ) {
241+ let left: u8 = left. parse ( ) ?;
242+ return if left == 0 {
243+ Ok ( Response :: new ( full ( "final" ) ) )
244+ } else {
245+ redirect ( 302 , & [ format ! ( "/redirect/chain/{}" , left - 1 ) ] )
246+ } ;
247+ }
248+ match path. as_str ( ) {
249+ "/redirect/non-utf8" => {
250+ return Ok ( Response :: builder ( )
251+ . status ( 302 )
252+ . header (
253+ "Location" ,
254+ hyper:: header:: HeaderValue :: from_bytes ( b"/bad\xff " ) ?,
255+ )
256+ . body ( full ( "invalid location" ) ) ?)
257+ }
258+ "/redirect/unicode" => {
259+ return Ok ( Response :: builder ( )
260+ . status ( 302 )
261+ . header (
262+ "Location" ,
263+ hyper:: header:: HeaderValue :: from_bytes (
264+ "/redirect/inspect?name=caf\u{e9} " . as_bytes ( ) ,
265+ ) ?,
266+ )
267+ . body ( full ( "unicode location" ) ) ?)
268+ }
269+ "/redirect/missing" => {
270+ return Ok ( Response :: builder ( )
271+ . status ( 302 )
272+ . body ( full ( "missing-location" ) ) ?)
273+ }
274+ "/redirect/duplicate" => {
275+ return redirect ( 302 , & [ "/redirect/final" . into ( ) , "/not-found" . into ( ) ] )
276+ }
277+ "/redirect/malformed" => return redirect ( 302 , & [ "http://[" . into ( ) ] ) ,
278+ "/redirect/unsupported" => return redirect ( 302 , & [ "ftp://example.com/file" . into ( ) ] ) ,
279+ "/redirect/not-found" => return redirect ( 302 , & [ "/not-found" . into ( ) ] ) ,
280+ "/redirect/loop" => return redirect ( 302 , & [ "/redirect/loop" . into ( ) ] ) ,
281+ "/redirect/fragment" => return redirect ( 302 , & [ "/redirect/inspect#never-send" . into ( ) ] ) ,
282+ _ => { }
283+ }
284+ let segments: Vec < _ > = path. trim_start_matches ( '/' ) . split ( '/' ) . collect ( ) ;
285+ if let [ "redirect" , status, target] = segments. as_slice ( ) {
286+ let status: u16 = status. parse ( ) ?;
287+ let location = match * target {
288+ "echo" => "/redirect/inspect" ,
289+ "relative" => "../inspect" ,
290+ "cross" => "http://localhost:9000/redirect/inspect" ,
291+ "network" => "//localhost:9000/redirect/inspect" ,
292+ _ => {
293+ return Ok ( Response :: builder ( )
294+ . status ( 404 )
295+ . body ( full ( "unknown redirect target" ) ) ?)
296+ }
297+ } ;
298+ return redirect ( status, & [ location. to_owned ( ) ] ) ;
299+ }
300+ Ok ( Response :: builder ( )
301+ . status ( 404 )
302+ . body ( full ( "unknown redirect route" ) ) ?)
303+ }
304+
172305#[ tokio:: main]
173306async fn main ( ) -> Result < ( ) , GenericError > {
174307 // `examples/http-client.roc` reaches this server by host name so the platform
0 commit comments