v2 / vlib / net / http / http.v
280 lines · 251 sloc · 9.77 KB · 2cc4d27d6292f4dddeb277b24ec3d176ce1eb716
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4@[has_globals]
5module http
6
7import net.urllib
8import time
9
10const max_redirects = 16 // safari max - other browsers allow up to 20
11
12const content_type_default = 'text/plain'
13
14const bufsize = 64 * 1024
15
16// FetchConfig holds configuration data for the fetch function.
17pub struct FetchConfig {
18pub mut:
19 url string
20 method Method = .get
21 header Header
22 data string
23 params map[string]string
24 cookies map[string]string
25 user_agent string = 'v.http'
26 user_ptr voidptr = unsafe { nil }
27 verbose bool
28 proxy &HttpProxy = unsafe { nil }
29 read_timeout i64 = 30 * time.second // timeout for reading the response; applies to plain http and to direct https requests
30 write_timeout i64 = 30 * time.second // timeout for writing the request; applies to plain http (write timeouts are not enforced on the SSL write path yet)
31
32 validate bool // set this to true, if you want to stop requests, when their certificates are found to be invalid
33 verify string // the path to a rootca.pem file, containing trusted CA certificate(s)
34 cert string // the path to a cert.pem file, containing client certificate(s) for the request
35 cert_key string // the path to a key.pem file, containing private keys for the client certificate(s)
36 in_memory_verification bool // if true, verify, cert, and cert_key are read from memory, not from a file
37 allow_redirect bool = true // whether to allow redirect
38 max_retries int = 5 // maximum number of retries required when an underlying socket error occurs
39 // callbacks to allow custom reporting code to run, while the request is running, and to implement streaming
40 on_redirect RequestRedirectFn = unsafe { nil }
41 on_progress RequestProgressFn = unsafe { nil }
42 on_progress_body RequestProgressBodyFn = unsafe { nil }
43 on_finish RequestFinishFn = unsafe { nil }
44
45 stop_copying_limit i64 = -1 // after this many bytes are received, stop copying to the response. Note that on_progress and on_progress_body callbacks, will continue to fire normally, until the full response is read, which allows you to implement streaming downloads, without keeping the whole big response in memory
46 stop_receiving_limit i64 = -1 // after this many bytes are received, break out of the loop that reads the response, effectively stopping the request early. No more on_progress callbacks will be fired. The on_finish callback will fire.
47}
48
49// new_request creates a new Request given the request `method`, `url_`, and
50// `data`.
51pub fn new_request(method Method, url_ string, data string) Request {
52 url := if method == .get && !url_.contains('?') { url_ + '?' + data } else { url_ }
53 // println('new req() method=${method} url="${url}" dta="${data}"')
54 return Request{
55 method: method
56 url: url
57 data: data
58 /*
59 headers: {
60 'Accept-Encoding': 'compress'
61 }
62 */
63 }
64}
65
66// get sends a GET HTTP request to the given `url`.
67pub fn get(url string) !Response {
68 return fetch(method: .get, url: url)
69}
70
71// post sends the string `data` as an HTTP POST request to the given `url`.
72pub fn post(url string, data string) !Response {
73 return fetch(
74 method: .post
75 url: url
76 data: data
77 header: new_header(key: .content_type, value: content_type_default)
78 )
79}
80
81// post_json sends the JSON `data` as an HTTP POST request to the given `url`.
82pub fn post_json(url string, data string) !Response {
83 return fetch(
84 method: .post
85 url: url
86 data: data
87 header: new_header(key: .content_type, value: 'application/json')
88 )
89}
90
91// post_form sends the map `data` as X-WWW-FORM-URLENCODED data to an HTTP POST request
92// to the given `url`.
93pub fn post_form(url string, data map[string]string) !Response {
94 return fetch(
95 method: .post
96 url: url
97 header: new_header(key: .content_type, value: 'application/x-www-form-urlencoded')
98 data: url_encode_form_data(data)
99 )
100}
101
102pub fn post_form_with_cookies(url string, data map[string]string, cookies map[string]string) !Response {
103 return fetch(
104 method: .post
105 url: url
106 header: new_header(key: .content_type, value: 'application/x-www-form-urlencoded')
107 data: url_encode_form_data(data)
108 cookies: cookies
109 )
110}
111
112@[params]
113pub struct PostMultipartFormConfig {
114pub mut:
115 form map[string]string
116 files map[string][]FileData
117 header Header
118}
119
120// post_multipart_form sends multipart form data `conf` as an HTTP POST
121// request to the given `url`.
122pub fn post_multipart_form(url string, conf PostMultipartFormConfig) !Response {
123 body, boundary := multipart_form_body(conf.form, conf.files)
124 mut header := conf.header
125 header.set(.content_type, 'multipart/form-data; boundary="${boundary}"')
126 return fetch(
127 method: .post
128 url: url
129 header: header
130 data: body
131 )
132}
133
134// put sends string `data` as an HTTP PUT request to the given `url`.
135pub fn put(url string, data string) !Response {
136 return fetch(
137 method: .put
138 url: url
139 data: data
140 header: new_header(key: .content_type, value: content_type_default)
141 )
142}
143
144// patch sends string `data` as an HTTP PATCH request to the given `url`.
145pub fn patch(url string, data string) !Response {
146 return fetch(
147 method: .patch
148 url: url
149 data: data
150 header: new_header(key: .content_type, value: content_type_default)
151 )
152}
153
154// head sends an HTTP HEAD request to the given `url`.
155pub fn head(url string) !Response {
156 return fetch(method: .head, url: url)
157}
158
159// delete sends an HTTP DELETE request to the given `url`.
160pub fn delete(url string) !Response {
161 return fetch(method: .delete, url: url)
162}
163
164// prepare prepares a new request for fetching, but does not call its .do() method.
165// It is useful, if you want to reuse request objects, for several requests in a row,
166// modifying the request each time, then calling .do() to get the new response.
167pub fn prepare(config FetchConfig) !Request {
168 if config.url == '' {
169 return error('http.fetch: empty url')
170 }
171 url := build_url_from_fetch(config) or { return error('http.fetch: invalid url ${config.url}') }
172 req := Request{
173 method: config.method
174 url: url
175 data: config.data
176 header: config.header
177 cookies: config.cookies
178 user_agent: config.user_agent
179 user_ptr: config.user_ptr
180 verbose: config.verbose
181 validate: config.validate
182 read_timeout: config.read_timeout
183 write_timeout: config.write_timeout
184 verify: config.verify
185 cert: config.cert
186 proxy: config.proxy
187 cert_key: config.cert_key
188 in_memory_verification: config.in_memory_verification
189 allow_redirect: config.allow_redirect
190 max_retries: config.max_retries
191 on_progress: config.on_progress
192 on_progress_body: config.on_progress_body
193 on_redirect: config.on_redirect
194 on_finish: config.on_finish
195 stop_copying_limit: config.stop_copying_limit
196 stop_receiving_limit: config.stop_receiving_limit
197 }
198 return req
199}
200
201// SchemeHandlerFn dispatches a `fetch()` call for a non-HTTP scheme. Used by
202// out-of-tree-friendly modules like `net.s3` to register themselves at init
203// time without forcing `net.http` to know about them statically.
204pub type SchemeHandlerFn = fn (config FetchConfig) !Response
205
206__global scheme_handlers = map[string]SchemeHandlerFn{}
207
208// register_scheme attaches `handler` as the dispatcher for URLs with the
209// given `scheme` (e.g. `'s3'`). Handlers are looked up by `fetch()` before
210// the native HTTP path runs. Modules typically call this from `init()`.
211pub fn register_scheme(scheme string, handler SchemeHandlerFn) {
212 scheme_handlers[scheme] = handler
213}
214
215// unregister_scheme removes a previously-registered scheme handler. Mostly
216// useful in tests that install a temporary handler.
217pub fn unregister_scheme(scheme string) {
218 scheme_handlers.delete(scheme)
219}
220
221fn scheme_of(url string) string {
222 colon := url.index(':') or { return '' }
223 if colon == 0 {
224 return ''
225 }
226 return url[..colon]
227}
228
229// TODO: @[noinline] attribute is used for temporary fix the 'get_text()' intermittent segfault / nil value when compiling with GCC 13.2.x and -prod option ( Issue #20506 )
230// fetch sends an HTTP request to the `url` with the given method and configuration.
231// When `config.url` uses a scheme registered via `register_scheme` (e.g.
232// `s3://`), the call is delegated to that handler instead of the native
233// HTTP path.
234@[noinline]
235pub fn fetch(config FetchConfig) !Response {
236 if scheme_handlers.len > 0 {
237 scheme := scheme_of(config.url)
238 if scheme != '' && scheme != 'http' && scheme != 'https' {
239 if h := scheme_handlers[scheme] {
240 return h(config)
241 }
242 }
243 }
244 req := prepare(config)!
245 return req.do()!
246}
247
248// get_text sends an HTTP GET request to the given `url` and returns the text content of the response.
249pub fn get_text(url string) string {
250 resp := fetch(url: url, method: .get) or { return '' }
251 return resp.body
252}
253
254// url_encode_form_data converts mapped data to a URL encoded string.
255pub fn url_encode_form_data(data map[string]string) string {
256 mut pieces := []string{}
257 for key_, value_ in data {
258 key := urllib.query_escape(key_)
259 value := urllib.query_escape(value_)
260 pieces << '${key}=${value}'
261 }
262 return pieces.join('&')
263}
264
265fn build_url_from_fetch(config FetchConfig) !string {
266 mut url := urllib.parse(config.url)!
267 if config.params.len == 0 {
268 return url.str()
269 }
270 mut pieces := []string{cap: config.params.len}
271 for key, val in config.params {
272 pieces << '${key}=${val}'
273 }
274 mut query := pieces.join('&')
275 if url.raw_query.len > 1 {
276 query = url.raw_query + '&' + query
277 }
278 url.raw_query = query
279 return url.str()
280}
281