Eio HTTP server with static file serving and route handlers
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Percent-decode query parameters in parse_url per RFC 3986 §2.1

parse_url now decodes %XX sequences and '+' (as space) in query
parameter names and values. Fixes double-encoding when OAuth callback
parameters like code=abc%2Fdef were re-encoded by the token exchange.

+54 -7
+37 -5
lib/respond.ml
··· 72 72 73 73 type params = (string * string) list 74 74 type post_request = { params : params; body : string; headers : Headers.t } 75 + type get_request = { params : params; headers : Headers.t } 75 76 76 77 type handler = 77 - | Get of (params -> Response.t) 78 + | Get of (get_request -> Response.t) 78 79 | Post of (post_request -> Response.t) 79 80 80 81 type route = { meth : [ `GET | `POST ]; path : string; handler : handler } ··· 139 140 140 141 (* ── URL parsing ──────────────────────────────────────────────────── *) 141 142 143 + (* Percent-decode per RFC 3986 §2.1: decode %XX sequences and '+' as space 144 + (application/x-www-form-urlencoded). *) 145 + let pct_decode s = 146 + let len = String.length s in 147 + let buf = Buffer.create len in 148 + let i = ref 0 in 149 + while !i < len do 150 + match s.[!i] with 151 + | '%' when !i + 2 < len -> ( 152 + let hi = s.[!i + 1] and lo = s.[!i + 2] in 153 + match (Char.lowercase_ascii hi, Char.lowercase_ascii lo) with 154 + | (('0' .. '9' | 'a' .. 'f') as h), (('0' .. '9' | 'a' .. 'f') as l) -> 155 + let hex c = 156 + Char.code c 157 + - if c <= '9' then Char.code '0' else Char.code 'a' - 10 158 + in 159 + Buffer.add_char buf (Char.chr ((hex h lsl 4) lor hex l)); 160 + i := !i + 3 161 + | _ -> 162 + Buffer.add_char buf '%'; 163 + incr i) 164 + | '+' -> 165 + Buffer.add_char buf ' '; 166 + incr i 167 + | c -> 168 + Buffer.add_char buf c; 169 + incr i 170 + done; 171 + Buffer.contents buf 172 + 142 173 let parse_url url = 143 174 match String.index_opt url '?' with 144 175 | None -> (url, []) ··· 149 180 String.split_on_char '&' qs 150 181 |> List.filter_map (fun pair -> 151 182 match String.index_opt pair '=' with 152 - | None -> if pair = "" then None else Some (pair, "") 183 + | None -> if pair = "" then None else Some (pct_decode pair, "") 153 184 | Some j -> 154 185 Some 155 - ( String.sub pair 0 j, 156 - String.sub pair (j + 1) (String.length pair - j - 1) )) 186 + ( pct_decode (String.sub pair 0 j), 187 + pct_decode 188 + (String.sub pair (j + 1) (String.length pair - j - 1)) )) 157 189 in 158 190 (path, params) 159 191 ··· 253 285 match match_route routes path with 254 286 | Some { handler = Get handler; _ } -> ( 255 287 try 256 - let r = handler params in 288 + let r = handler { params; headers } in 257 289 Log.info (fun m -> 258 290 m "%s %s %s" meth_str path (status_line r.Response.status)); 259 291 send_response flow r
+4 -1
lib/respond.mli
··· 47 47 48 48 (** {1 Routes} *) 49 49 50 + type get_request = { params : params; headers : Headers.t } 51 + (** GET request with query parameters and parsed headers. *) 52 + 50 53 type post_request = { params : params; body : string; headers : Headers.t } 51 54 (** POST request with raw body and parsed headers. *) 52 55 53 56 type route 54 57 (** A typed route: method + path + handler. *) 55 58 56 - val get : string -> (params -> Response.t) -> route 59 + val get : string -> (get_request -> Response.t) -> route 57 60 (** [get path handler] handles GET requests at [path]. *) 58 61 59 62 val post : string -> (post_request -> Response.t) -> route
+13 -1
test/test_respond.ml
··· 53 53 let test_url_encoded_chars () = 54 54 let path, params = Respond.parse_url "/search?q=hello%20world" in 55 55 check_string "path" "/search" path; 56 - check_params "encoded space" [ ("q", "hello%20world") ] params 56 + check_params "decoded space" [ ("q", "hello world") ] params 57 + 58 + let test_url_plus_as_space () = 59 + let _path, params = Respond.parse_url "/s?q=hello+world" in 60 + check_params "plus as space" [ ("q", "hello world") ] params 61 + 62 + let test_url_pct_roundtrip () = 63 + let _path, params = Respond.parse_url "/cb?code=abc%2Fdef&state=x%3Dy" in 64 + check_params "decoded slash and equals" 65 + [ ("code", "abc/def"); ("state", "x=y") ] 66 + params 57 67 58 68 let test_url_empty_value () = 59 69 let _path, params = Respond.parse_url "/f?key=" in ··· 88 98 ("empty pairs", `Quick, test_parse_url_empty_pairs); 89 99 ("fragment in path", `Quick, test_url_fragment); 90 100 ("percent-encoded", `Quick, test_url_encoded_chars); 101 + ("plus as space", `Quick, test_url_plus_as_space); 102 + ("pct roundtrip", `Quick, test_url_pct_roundtrip); 91 103 ("empty value", `Quick, test_url_empty_value); 92 104 ("multiple same key", `Quick, test_url_multiple_same_key); 93 105 ("long query", `Quick, test_url_long_query);