v / vlib / net / http / http.v
284 lines · 255 sloc · 10.61 KB · 065a450b86f6459b1e4398fe7b0594bbfcc2d691
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 enable_http2 bool = true // when true (the default) and the URL is https, advertise ALPN `h2, http/1.1` and use HTTP/2 if the server selects it; set to false to force HTTP/1.1. Ignored for plain http://, and for the Windows SChannel backend which has no ALPN yet (see vlang/v#27383). on_progress / on_progress_body / stop_copying_limit / stop_receiving_limit are honored on the HTTP/2 path; on_progress fires per DATA frame payload rather than per raw network read.
40 disable_connection_reuse bool // opt out of the shared connection pool: open a fresh connection for this request, send `Connection: close`, and close the connection after the response (the pre-pooling behavior)
41 // callbacks to allow custom reporting code to run, while the request is running, and to implement streaming
42 on_redirect RequestRedirectFn = unsafe { nil }
43 on_progress RequestProgressFn = unsafe { nil }
44 on_progress_body RequestProgressBodyFn = unsafe { nil }
45 on_finish RequestFinishFn = unsafe { nil }
46
47 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
48 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.
49}
50
51// new_request creates a new Request given the request `method`, `url_`, and
52// `data`.
53pub fn new_request(method Method, url_ string, data string) Request {
54 url := if method == .get && !url_.contains('?') { url_ + '?' + data } else { url_ }
55 // println('new req() method=${method} url="${url}" dta="${data}"')
56 return Request{
57 method: method
58 url: url
59 data: data
60 /*
61 headers: {
62 'Accept-Encoding': 'compress'
63 }
64 */
65 }
66}
67
68// get sends a GET HTTP request to the given `url`.
69pub fn get(url string) !Response {
70 return fetch(method: .get, url: url)
71}
72
73// post sends the string `data` as an HTTP POST request to the given `url`.
74pub fn post(url string, data string) !Response {
75 return fetch(
76 method: .post
77 url: url
78 data: data
79 header: new_header(key: .content_type, value: content_type_default)
80 )
81}
82
83// post_json sends the JSON `data` as an HTTP POST request to the given `url`.
84pub fn post_json(url string, data string) !Response {
85 return fetch(
86 method: .post
87 url: url
88 data: data
89 header: new_header(key: .content_type, value: 'application/json')
90 )
91}
92
93// post_form sends the map `data` as X-WWW-FORM-URLENCODED data to an HTTP POST request
94// to the given `url`.
95pub fn post_form(url string, data map[string]string) !Response {
96 return fetch(
97 method: .post
98 url: url
99 header: new_header(key: .content_type, value: 'application/x-www-form-urlencoded')
100 data: url_encode_form_data(data)
101 )
102}
103
104pub fn post_form_with_cookies(url string, data map[string]string, cookies map[string]string) !Response {
105 return fetch(
106 method: .post
107 url: url
108 header: new_header(key: .content_type, value: 'application/x-www-form-urlencoded')
109 data: url_encode_form_data(data)
110 cookies: cookies
111 )
112}
113
114@[params]
115pub struct PostMultipartFormConfig {
116pub mut:
117 form map[string]string
118 files map[string][]FileData
119 header Header
120}
121
122// post_multipart_form sends multipart form data `conf` as an HTTP POST
123// request to the given `url`.
124pub fn post_multipart_form(url string, conf PostMultipartFormConfig) !Response {
125 body, boundary := multipart_form_body(conf.form, conf.files)
126 mut header := conf.header
127 header.set(.content_type, 'multipart/form-data; boundary="${boundary}"')
128 return fetch(
129 method: .post
130 url: url
131 header: header
132 data: body
133 )
134}
135
136// put sends string `data` as an HTTP PUT request to the given `url`.
137pub fn put(url string, data string) !Response {
138 return fetch(
139 method: .put
140 url: url
141 data: data
142 header: new_header(key: .content_type, value: content_type_default)
143 )
144}
145
146// patch sends string `data` as an HTTP PATCH request to the given `url`.
147pub fn patch(url string, data string) !Response {
148 return fetch(
149 method: .patch
150 url: url
151 data: data
152 header: new_header(key: .content_type, value: content_type_default)
153 )
154}
155
156// head sends an HTTP HEAD request to the given `url`.
157pub fn head(url string) !Response {
158 return fetch(method: .head, url: url)
159}
160
161// delete sends an HTTP DELETE request to the given `url`.
162pub fn delete(url string) !Response {
163 return fetch(method: .delete, url: url)
164}
165
166// prepare prepares a new request for fetching, but does not call its .do() method.
167// It is useful, if you want to reuse request objects, for several requests in a row,
168// modifying the request each time, then calling .do() to get the new response.
169pub fn prepare(config FetchConfig) !Request {
170 if config.url == '' {
171 return error('http.fetch: empty url')
172 }
173 url := build_url_from_fetch(config) or { return error('http.fetch: invalid url ${config.url}') }
174 req := Request{
175 method: config.method
176 url: url
177 data: config.data
178 header: config.header
179 cookies: config.cookies
180 user_agent: config.user_agent
181 user_ptr: config.user_ptr
182 verbose: config.verbose
183 validate: config.validate
184 read_timeout: config.read_timeout
185 write_timeout: config.write_timeout
186 verify: config.verify
187 cert: config.cert
188 proxy: config.proxy
189 cert_key: config.cert_key
190 in_memory_verification: config.in_memory_verification
191 allow_redirect: config.allow_redirect
192 max_retries: config.max_retries
193 enable_http2: config.enable_http2
194 disable_connection_reuse: config.disable_connection_reuse
195 on_progress: config.on_progress
196 on_progress_body: config.on_progress_body
197 on_redirect: config.on_redirect
198 on_finish: config.on_finish
199 stop_copying_limit: config.stop_copying_limit
200 stop_receiving_limit: config.stop_receiving_limit
201 }
202 return req
203}
204
205// SchemeHandlerFn dispatches a `fetch()` call for a non-HTTP scheme. Used by
206// out-of-tree-friendly modules like `net.s3` to register themselves at init
207// time without forcing `net.http` to know about them statically.
208pub type SchemeHandlerFn = fn (config FetchConfig) !Response
209
210__global scheme_handlers = map[string]SchemeHandlerFn{}
211
212// register_scheme attaches `handler` as the dispatcher for URLs with the
213// given `scheme` (e.g. `'s3'`). Handlers are looked up by `fetch()` before
214// the native HTTP path runs. Modules typically call this from `init()`.
215pub fn register_scheme(scheme string, handler SchemeHandlerFn) {
216 scheme_handlers[scheme] = handler
217}
218
219// unregister_scheme removes a previously-registered scheme handler. Mostly
220// useful in tests that install a temporary handler.
221pub fn unregister_scheme(scheme string) {
222 scheme_handlers.delete(scheme)
223}
224
225fn scheme_of(url string) string {
226 colon := url.index(':') or { return '' }
227 if colon == 0 {
228 return ''
229 }
230 return url[..colon]
231}
232
233// 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 )
234// fetch sends an HTTP request to the `url` with the given method and configuration.
235// When `config.url` uses a scheme registered via `register_scheme` (e.g.
236// `s3://`), the call is delegated to that handler instead of the native
237// HTTP path.
238@[noinline]
239pub fn fetch(config FetchConfig) !Response {
240 if scheme_handlers.len > 0 {
241 scheme := scheme_of(config.url)
242 if scheme != '' && scheme != 'http' && scheme != 'https' {
243 if h := scheme_handlers[scheme] {
244 return h(config)
245 }
246 }
247 }
248 req := prepare(config)!
249 return req.do()!
250}
251
252// get_text sends an HTTP GET request to the given `url` and returns the text content of the response.
253pub fn get_text(url string) string {
254 resp := fetch(url: url, method: .get) or { return '' }
255 return resp.body
256}
257
258// url_encode_form_data converts mapped data to a URL encoded string.
259pub fn url_encode_form_data(data map[string]string) string {
260 mut pieces := []string{}
261 for key_, value_ in data {
262 key := urllib.query_escape(key_)
263 value := urllib.query_escape(value_)
264 pieces << '${key}=${value}'
265 }
266 return pieces.join('&')
267}
268
269fn build_url_from_fetch(config FetchConfig) !string {
270 mut url := urllib.parse(config.url)!
271 if config.params.len == 0 {
272 return url.str()
273 }
274 mut pieces := []string{cap: config.params.len}
275 for key, val in config.params {
276 pieces << '${key}=${val}'
277 }
278 mut query := pieces.join('&')
279 if url.raw_query.len > 1 {
280 query = url.raw_query + '&' + query
281 }
282 url.raw_query = query
283 return url.str()
284}
285