Let’s assume for a moment that the resource you are querying is returning a set of entries that do contain certain properties. If you don’t specify a filter you will basically get a (pageable) representation of those entries either as embedded objects or as links to those resources.
Now you want to filter the results based on some properties these entries have. Most programming languages nowadays provide some lambda functionality in the form of
List filteredList = list.filter(item => item.propertyX == ...)...;
The result of such a filter function is usually a list of items that fulfilled the specified conditions. If no items met the given condition then the result will be an empty list.
Applying certain filter conditions on the Web can be designed similarly. Is it really an error when a provided filter expression doesn’t yield any entries? IMO it is not an error in terms of the actual message transport itself as the server was able to receive and parse the request without any issues. As such it has to be some kind of business rule that states that only admissible values are allowed as input.
If you or your company consider the case of a provided filter value for a property returning no results as an error or you perform some i.e. XML or JSON schemata validation on the received payload (for complex requests) then we should look at how those mentioned HTTP errors are defined:
The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). (Source: RFC 7230)
Here, it is clearly the case that you don’t want to process requests that come with an invalid property value and as such decline the request as such.
The 422 (Unprocessable Entity) status code means the server understands the content type of the request entity (hence a 415(Unsupported Media Type) status code is inappropriate), and the syntax of the request entity is correct (thus a 400 (Bad Request) status code is inappropriate) but was unable to process the contained instructions. For example, this error condition may occur if an XML request body contains well-formed (i.e., syntactically correct), but semantically erroneous, XML instructions. (Source: RFC 4918 (WebDAV))
In this case you basically say that the payload was actually syntactically correct but failed on a semmantical level.
Note that 422 Unprocessable Entity
stems from WebDAV while 400 Bad Request
is defined in the HTTP specification. This can have some impact if your API serves arbitrary HTTP clients. Ones that only know and support the HTTP error codes defined in the HTTP sepcification won’t be able to really determine the semantics of the 422
response. They will still consider it as a user error, but won’t be able to provide the client with any more help on that issue. As such, if your API needs to be as generic as possible, stick to 400 Bad Request
. If you are sure all clients support 422 Unprocessable Entity
go for that one.
General improvement hints
As you tagged your question with rest, let’s see how we can improve this case.
REST is an architectural style with an intention of decoupling clients from servers to make the former one more failure tollerant while allowing the latter one to evolve freely over time. Servers in such architectures should therefore provide clients with all the things clients need to make valid requests. To avoid having clients to know upfront what a server expects as input, servers usually provide some kind of input mask clients can use to fill in stuff the server needs.
On the browsable Web this is usually accomplished by HTML Forms. The form not only teaches your client where to send the request to, which HTTP operation to use and which representation format the request should actually use (usually given implicitly as application/x-www-form-urlencoded
) but also the sturcture and properties the server supports.
In HTML forms it is rather easy for the server to restrict the input choices of a client by using something along the lines of
<form action="/target">
<label for="cars">Choose a car:</label>
<select name="cars" id="cars">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="opel">Opel</option>
<option value="audi">Audi</option>
</select>
<br/>
<input type="submit" value="Submit">
</form>
This doesn’t really remove the needs to check and verify the correctness of the request on the server side, tough you make it much easier for the client to actually perform a valid request.
Unfortunately, HTML forms itself have their limits. I.e. they only allow POST
and GET
requests to be issues. While encType
defaults to application/x-www-form-urlencoded
, if you want to transfer files you should use multipart/form-data
. Other than that, any valid content-type should be admissible.
If you prefer JSON-based payloads over HTML you might want to look into JSON Forms, HAL forms, Ion Forms among others.
Note though that you should adhere to the content type negotiation principles. Most often proactive content type negotiation is performed where a client sends its preferences within the Accept
header and the server will select the best match somehow and return either the resource mapped to that representation format or respond with a 406 Not Acceptable
response. While the standard doesn’t prevent returning a default representation in such caes, it bears the danger that clients won’t be able to process such responses then. A better alternative here would be to fall back to reactive negotiation where the server responds with a 300 Muliple Choice
response where a client has to select one of the provided alternatives and then send a GET
request to the selected alternative URIs to retrieve the content in the payload may be able to process.
If you want to provide a simple link a client can use to retrieve filtered results, the server should provide the client already with the full URI as well as a link relation name and/or extension relation type that the client can use to lookup the URI to retrieve the content for if interested in.
Both, forms and link-relation support, fall under the HATEOAS umbrella as they help to remove the need for any external documentation such as OpenAPI or Swagger documentation.
To sum things up, I would rethink whether a provided property value that does not exist should really end up as a business failure. I think returning an empty list is just fine here as you clearly state that way that for the given criterias no result was obtainable. If you though still want to stick to a business error check what clients actually make use of your API. If they support 422
go for that one. If you don’t know, better stick to 400
as it should be understood by all HTTP clients equally.
In oder to remove the likelihood of ending up with requests that issue invalid property values, use forms to teach clients how requests should look like. Through certain elements or properties you can already teach a client that only a limited set of choices is valid for a certain property. Instead of a form you could also provide dedicated links a client can just use to obtain the filtered result. Just make sure to issue those links with meaningful link relatin names then.
The spring boot error Required string parameter is not present occurs when the request parameter is not stated in the request url and the request parameter is configured as mandatory. The controller method needs to provide a value for the request parameter. The invoked url does not contain the value of the request parameter. If the value for the @RequestParam is not given, the exception Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter ‘name’ is not present] will be thrown.
The request param is configured using the annotation @RequestParam in spring boot. By default, the value of the @RequestParam annotation is mandatory.
In a rest call, request parameter can be made as mandatory by setting required=true. If a parameter is mandatory and the parameter value is not received by restcontroller, then this warning message is thrown in the application. Browser shows as There was an unexpected error (type=Bad Request, status=400).
Exception
2019-10-03 17:40:07.516 WARN 4304 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter 'name' is not present]
Error Message
Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Thu Oct 03 17:42:47 IST 2019 There was an unexpected error (type=Bad Request, status=400). Required String parameter 'name' is not present
How to reproduce this issue
In Spring Boot Application, create a rest controller class. Create a method with a @Requestparam as a parameter in the rest controller class. Configure the request param as required=true. Now call this rest url without passing value to this request param.
package com.yawintutor.application;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("welcome")
public String firstPage(@RequestParam(name = "name", required = true) String name) {
return new String( "Welcome to "+name+"!");
}
}
Output
$ curl -X GET http://localhost:8080/welcome?username=Yawin
{"timestamp":"2020-12-30T22:50:10.246+0000","status":400,"error":"Bad Request","message":"Required String parameter 'name' is not present","path":"/welcome"}
Root Cause
The request url does not contain the value of the request parameter. The request parameter is configured as the required parameter in the spring boot rest controller. When the request call to the controller method is reached, the error Required string parameter is not present is thrown as the required request parameter is not available.
Solution 1
The request parameter is added using the @Requestparam annotation. If the “required” attribute of the @RequestParam annotation is configured as true, the controller method expects the value for the request param. If the param value is an optional value, configure as required=false in the @RequestParam annotation.
In the example below, the required attribute is set to false so that the request parameter value is optional.
package com.yawintutor.application;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("welcome")
public String firstPage(@RequestParam(name = "name", required = false) String name) {
return new String( "Welcome to "+name+"!");
}
}
Output
In this case, the value of the request param ‘name’ is null.
$ curl -X GET http://localhost:8080/welcome?username=Yawin
Welcome to null!
Solution 2
When the rest controller method requires a request param value and the request url can not send a request parameter, the @RequestParam annotation allows the default value to be configured. If the url has the value of the request param, the value will be used in the method. If the value of the request param is not passed in the url, the default value is used in the method.
package com.yawintutor.application;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("welcome")
public String firstPage(@RequestParam(name = "name", required = false, defaultValue="unknown") String name) {
return new String( "Welcome to "+name+"!");
}
}
Output
$ curl -X GET http://localhost:8080/welcome?username=Yawin
Welcome to unknown!
Solution 3
The request param value is configured as required in the @RequestParam annotation of the rest controller method. The value of the request param must be sent to the url. The request param should be passed to the url as the key = value format.
The example below shows how to configure the request param in the url request.
$ curl -X GET http://localhost:8080/welcome?name=Yawin
Welcome to Yawin!
Содержание
- Введение в тему
- Создание get и post запроса
- Передача параметров в url
- Содержимое ответа response
- Бинарное содержимое ответа
- Содержимое ответа в json
- Необработанное содержимое ответа
- Пользовательские заголовки
- Более сложные post запросы
- Post отправка multipart encoded файла
- Коды состояния ответа
- Заголовки ответов
- Cookies
- Редиректы и история
- Тайм ауты
- Ошибки и исключения
Введение в тему
Модуль python requests – это общепринятый стандарт для работы с запросами по протоколу HTTP.
Этот модуль избавляет Вас от необходимости работать с низкоуровневыми деталями. Работа с запросами становится простой и элегантной.
В этом уроке будут рассмотрены самые полезные функций библиотеки requests и различные способы их использования.
Перед использованием модуля его необходимо установить:
Создание get и post запроса
Сперва необходимо добавить модуль Requests в Ваш код:
Создадим запрос и получим ответ, содержащий страницу и все необходимые данные о ней.
import requests
response = requests.get('https://www.google.ru/')
В переменную response попадает ответ на запрос. Благодаря этому объекту можно использовать любую информацию, относящуюся к этому ответу.
Сделать POST запрос так же очень просто:
import requests
response = requests.post('https://www.google.ru/', data = {'foo':3})
Другие виды HTTP запросов, к примеру: PUT, DELETE, и прочих, выполнить ничуть не сложнее:
import requests
response = requests.put('https://www.google.ru/', data = {'foo':3})
response = requests.delete('https://www.google.ru/')
response = requests.head('https://www.google.ru/')
response = requests.options('https://www.google.ru/')
Передача параметров в url
Иногда может быть необходимо отправить различные данные вместе с запросом URL. При ручной настройке URL, параметры выглядят как пары ключ=значение после знака «?». Например, https://www.google.ru/search?q=Python. Модуль Requests предоставляет возможность передать эти параметры как словарь, применяя аргумент params. Если вы хотите передать q = Python и foo=’bar’ ресурсу google.ru/search, вы должны использовать следующий код:
import requests
params_dict = {'q':'Python', 'foo':'bar'}
response = requests.get('https://www.google.ru/search', params=params_dict)
print(response.url)
#Вывод:
https://www.google.ru/search?q=Python&foo=bar
Здесь мы видим, что URL был сформирован именно так, как это было задумано.
Пара ключ=значение, где значение равняется None, не будет добавлена к параметрам запроса URL.
Так же есть возможность передавать в запрос список параметров:
import requests
params_dict = {'q':'Python', 'foo':['bar', 'eggs']}
response = requests.get('https://www.google.ru/search', params=params_dict)
print(response.url)
#Вывод:
https://www.google.ru/search?q=Python&foo=bar&foo=eggs
Содержимое ответа response
Код из предыдущего листинга создаёт объект Response, содержащий ответ сервера на наш запрос. Обратившись к его атрибуту .url можно просмотреть адрес, куда был направлен запрос. Атрибут .text позволяет просмотреть содержимое ответа. Вот как это работает:
import requests
params_dict = {'q':'Python'}
response = requests.get('https://www.google.ru/search', params=params_dict)
print(response.text)
#Вывод:<!doctype html><html lang="ru"><head><meta charset="UTF-8"><meta content="/images/branding/googleg/1x/googleg_standard_color_128dp.png"…
Библиотека автоматически пытается определить кодировку ответа основываясь на его заголовках. Узнать, какую кодировку выбрал модуль, можно следующим образом:
import requests
params_dict = {'q':'Python'}
response = requests.get('https://www.google.ru/search', params=params_dict)
print(response.encoding)
#Вывод:
windows-1251
Можно так же самостоятельно установить кодировку используя атрибут .encoding.
import requests
params_dict = {'q':'Python'}
response = requests.get('https://www.google.ru/search', params=params_dict)
response.encoding = 'utf-8' # указываем необходимую кодировку вручную
print(response.encoding)
#Вывод:
utf-8
Бинарное содержимое ответа
Существует возможность просмотра ответа в виде байтов:
import requests
params_dict = {'q':'Python'}
response = requests.get('https://www.google.ru/search', params=params_dict)
print(response.content)
#Вывод:
b'<!doctype html><html lang="ru"><head><meta charset="UTF-8"><meta content="/images/branding/googleg/1x/googleg_standard_color_128dp.png" …
При передаче со сжатием ответ автоматически декодируется для Вас.
Содержимое ответа в json
Так же в Requests есть встроенная обработка ответов в формате JSON:
import requests
import json
response = requests.get(‘http://api.open-notify.org/astros.json’)
print(json.dumps(response.json(), sort_keys=True, indent=4))
#Вывод:
{
«message»: «success»,
«number»: 10,
«people»: [
{
«craft»: «ISS»,
«name»: «Mark Vande Hei»
},
{
«craft»: «ISS»,
«name»: «Oleg Novitskiy»
},
…
[/dm_code_snippet]
Если ответ не является JSON, то .json выбросит исключение:
import requests
import json
response = requests.get('https://www.google.ru/search')
print(json.dumps(response.json(), sort_keys=True, indent=4))
#Вывод:
…
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Необработанное содержимое ответа
Если Вам нужно получить доступ к ответу сервера в чистом виде на уровне сокета, обратитесь к атрибуту .raw. Для этого необходимо указать параметр stream=True в запросе. Этот параметр заставляет модуль читать данные по мере их прибытия.
import requests
response = requests.get('https://www.google.ru/', stream=True)
print(response.raw)
print('Q'*10)
print(response.raw.read(15))
#Вывод:
<urllib3.response.HTTPResponse object at 0x000001E368771FA0>
QQQQQQQQQQ
b'x1fx8bx08x00x00x00x00x00x02xffxc5[[sxdb'
Так же можно использовать метод .iter_content. Этот метод итерирует данные потокового ответа и это позволяет избежать чтения содержимого сразу в память для больших ответов. Параметр chunk_size – это количество байтов, которые он должен прочитать в памяти. Параметр chunk_size можно произвольно менять.
import requests
response = requests.get('https://www.google.ru/', stream=True)
print(response.iter_content)
print('Q'*10)
print([i for i in response.iter_content(chunk_size=256)])
#Вывод:
<bound method Response.iter_content of <Response [200]>>
QQQQQQQQQQ
[b'<!doctype html><html itemscope="" itemtype="http://sche', b'ma.org/WebPage" lang="ru"><head><meta content=…
response.iter_content будет автоматически декодировать сжатый ответ. Response.raw — чистый набор байтов, неизменённое содержимое ответа.
Пользовательские заголовки
Если необходимо установить заголовки в HTTP запросе, передайте словарь с ними в параметр headers. Значения заголовка должны быть типа string, bytestring или unicode. Имена заголовков не чувствительны к регистру символов.
В следующем примере мы устанавливаем информацию об используемом браузере:
import requests
response = requests.get('https://www.google.ru/', headers={'user-agent': 'unknown_browser'})
print(response.request.headers)
# Вывод:
{'user-agent': 'unknown_browser', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
Более сложные post запросы
Существует способ отправить данные так, будто это результат заполнения формы на сайте:
import requests
response = requests.post('https://httpbin.org/post', data={'foo': 'bar'})
print(response.text)
# Вывод:
{
"args": {},
"data": "",
"files": {},
"form": {
"foo": "bar"
},
"headers": {
…
Параметр data может иметь произвольное количество значений для каждого ключа. Для этого необходимо указать data в формате кортежа, либо в виде dict со списками значений.
import requests
response = requests.post('https://httpbin.org/post', data={'foo':['bar', 'eggs']})
print(response.json()['form'])
print('|'*10)
response = requests.post('https://httpbin.org/post', data=[('foo', 'bar'), ('foo', 'eggs')])
print(response.json()['form'])
# Вывод:
{'foo': ['bar', 'eggs']}
||||||||||
{'foo': ['bar', 'eggs']}
Если нужно отправить данные, не закодированные как данные формы, то передайте в запрос строку вместо словаря. Тогда данные отправятся в изначальном виде.
import requests
response = requests.post('https://httpbin.org/post', data={'foo': 'bar'})
print('URL:', response.request.url)
print('Body:', response.request.body)
print('-' * 10)
response = requests.post('https://httpbin.org/post', data='foo=bar')
print('URL:', response.request.url)
print('Body:', response.request.body)
# Вывод:
URL: https://httpbin.org/post
URL: https://httpbin.org/post
Body: foo=bar
----------
URL: https://httpbin.org/post
Body: foo=bar
Post отправка multipart encoded файла
Запросы упрощают загрузку файлов с многостраничным кодированием (Multipart-Encoded):
import requests
url = 'https://httpbin.org/post'
files = {'file': open('report.xls', 'rb')}
response = requests.post(url, files=files)
print(response.text)
# Вывод:
{
...
"files": {
"file": "<censored...binary...data>"
},
...
}
Вы можете установить имя файла, content_type и заголовки в явном виде:
import requests
url = 'https://httpbin.org/post'
files = {'file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel', {'Expires': '0'})}
response = requests.post(url, files=files)
print(response.text)
# Вывод:
{
...
"files": {
"file": "<censored...binary...data>"
},
...
}
Можете отправить строки, которые будут приняты в виде файлов:
import requests
url = 'https://httpbin.org/post'
files = {'file': ('report.csv', 'some,data,to,sendnanother,row,to,sendn')}
response = requests.post(url, files=files)
print(response.text)
# Вывод:
{
...
"files": {
"file": "some,data,to,send\nanother,row,to,send\n"
},
...
}
Коды состояния ответа
Возможно, наиболее важные данные (первые – уж точно), которые вы можете получить, используя библиотеку requests, является код состояния ответа.
Так, 200 статус означает, что запрос выполнен успешно, тогда как 404 статус означает, что ресурс не найден.
Важнее всего то, с какой цифры начинается код состояния:
- 1XX — информация
- 2XX — успешно
- 3XX — перенаправление
- 4XX — ошибка клиента (ошибка на нашей стороне)
- 5XX — ошибка сервера (самые страшные коды для разработчика)
Используя атрибут .status_code можно получить статус, который вернул сервер:
import requests
response = requests.get('https://www.google.ru/')
print(response.status_code)
# Вывод:
200
.status_code вернул 200 — это означает, что запрос успешно выполнен и сервер вернул запрашиваемые данные.
При желании, такую информацию можно применить в Вашем Пайтон скрипте для принятия решений:
import requests
response = requests.get('https://www.google.ru/')
if response.status_code == 200: print('Успех!')elif response.status_code == 404: print('Страница куда-то пропала…')
# Вывод:
Успех!
Если код состояния response равен 200, то скрипт выведет «Успех!», но, если он равен 404, то скрипт вернёт «Страница куда-то пропала…».
Если применить модуль Response в условном выражении и проверить логическое значение его экземпляра (if response) то он продемонстрирует значение True, если код ответа находится в диапазоне между 200 и 400, и False во всех остальных случаях.
Упростим код из предыдущего примера:
import requests
response = requests.get('https://www.google.ru/fake/')
if response:
print('Успех!')
else:
print('Хьюстон, у нас проблемы!')
# Вывод:
Хьюстон, у нас проблемы!
Данный способ не проверяет, что код состояния равен именно 200.
Причиной этого является то, что response с кодом в диапазоне от 200 до 400, такие как 204 и 304, тоже являются успешными, ведь они возвращают обрабатываемый ответ. Следовательно, этот подход делит все запросы на успешные и неуспешные – не более того. Во многих случаях Вам потребуется более детальная обработка кодов состояния запроса.
Вы можете вызвать exception, если requests.get был неудачным. Такую конструкцию можно создать вызвав .raise_for_status() используя конструкцию try- except:
import requests
from requests.exceptions import HTTPError
for url in ['https://www.google.ru/', 'https://www.google.ru/invalid']:
try:
response = requests.get(url)
response.raise_for_status()
except HTTPError:
print(f'Возникла ошибка HTTP: {HTTPError}')
except Exception as err:
print(f'Возникла непредвиденная ошибка: {err}')
else:
print('Успех!')
# Вывод:
Успех!
Возникла ошибка HTTP: <class 'requests.exceptions.HTTPError'>
Заголовки ответов
Мы можем просматривать заголовки ответа сервера:
import requests
response = requests.get('https://www.google.ru/')
print(response.headers)
# Вывод:
{'Date': 'Sun, 27 Jun 2021 13:43:17 GMT', 'Expires': '-1', 'Cache-Control': 'private, max-age=0', 'Content-Type': 'text/html; charset=windows-1251', 'P3P': 'CP="This is not a P3P policy! See g.co/p3phelp for more info."', 'Content-Encoding': 'gzip', 'Server': 'gws', 'X-XSS-Protection': '0', 'X-Frame-Options': …
Cookies
Можно просмотреть файлы cookie, которые сервер отправляет вам обратно с помощью атрибута .cookies. Запросы также позволяют отправлять свои собственные cookie-файлы.
Чтобы добавить куки в запрос, Вы должны использовать dict, переданный в параметр cookie.
import requests
url = 'https://www.google.ru/'
headers = {'user-agent': 'your-own-user-agent/0.0.1'}
cookies = {'visit-month': 'February'}
response = requests.get(url, headers=headers, cookies=cookies)
print(response.request.headers)
# Вывод:
{'user-agent': 'your-own-user-agent/0.0.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Cookie': 'visit-month=February'}
Редиректы и история
По умолчанию модуль Requests выполняет редиректы для всех HTTP глаголов, кроме HEAD.
Существует возможность использовать параметр history объекта Response, чтобы отслеживать редиректы.
Например, GitHub перенаправляет все запросы HTTP на HTTPS:
import requests
response = requests.get('https://www.google.ru/')
print(response.url)
print(response.status_code)
print(response.history)
# Вывод:
https://www.google.ru/
200
[]
Тайм ауты
Так же легко можно управлять тем, сколько программа будет ждать возврат response. Время ожидания задаётся параметром timeout. Это очень важный параметр, так как, если его не использовать, написанный Вами скрипт может «зависнуть» в вечном ожидании ответа от сервера. Используем предыдущий код:
import requests
response = requests.get(‘https://www.google.ru/’, timeout=0.001)
print(response.url)
print(response.status_code)
print(response.history)
# Вывод:…
raise ConnectTimeout(e, request=request)
requests.exceptions.ConnectTimeout: HTTPSConnectionPool(host=’www.google.ru’, port=443): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPSConnection object at 0x000001E331681C70>, ‘Connection to www.google.ru timed out. (connect timeout=0.001)’))
Модуль не ждёт полной загрузки ответа. Исключение возникает, если сервер не отвечает (хотя бы один байт) за указанное время.
Ошибки и исключения
Если возникнет непредвиденная ситуация – ошибка соединения, модуль Requests выбросит эксепшн ConnectionError.
response.raise_for_status() возвращает объект HTTPError, если в процессе произошла ошибка. Его применяют для отладки модуля и, поэтому, он является неотъемлемой частью запросов Python.
Если выйдет время запроса, вызывается исключение Timeout. Если слишком много перенаправлений, то появится исключение TooManyRedirects.
JavaScript being a weakly typed language makes it is really hard for developers to validate any type of variables.
Lack of consistency and a well-defined framework doesn’t help either.
1. A middleware that can validate the request parameters.
2. What is a middleware?
3. Required Parameter check
4. Type check
5. Other validation checks
6. Checking the performance of Simple validator
7. Writing tests for simple request parameter validation middleware
8. Using Joi as an alternative to adding request parameter validator
9. Checking the performance of JOI validator
10. Using ajv as an alternative to adding request parameter validator
11. Checking the performance of AJV validator
12. Edit 1: Using Express-Validation for the validation
A few months ago while working on big Node.js application, handling millions of requests per hour, I noticed it too.
Every API was literally checking if a certain parameter was being passed by the frontend correctly.
The same validations were repeated all over the codebase again and again.
Every time I had to create a new route/new API endpoint I had to write the same validation code.
For a OOP based programmer turned JS developer this was a bit of a shock.
For example:
// Inside the route.
if (!request.phone_number) {
throw new Error('Main request parameter not present.');
}
In this post we are going to learn the simplest way in which you can validate your request parameter or any other thing for that matter.
If you are starting out your app, this might look okay to you but as your app keeps getting bigger and bigger, you might want to write some specific validation code that can handle this for you out of the box.
If you’re just getting started for your application, I would suggest you skip this article( But do subscribe so that you can receive other good stuff that I write about) and come back to it when you have some good number of developers editing your code.
This article is for someone who wants to wrap all his requests with some special validations.
This will also be helpful for your backend developers to know which parameters are required for the given route and which of them are optional. All you need to do is create a middleware which will keep account of all your parameters.
A good example for which middleware can be used is logging. You can log your request parameters, headers, response data to whatever logging system you are using.
We are only going to talk about a specific type of middleware in this article, but you can extend it to anything you want to.
A middleware that can validate the request parameters.
How would you feel when you don’t have to worry about the incoming request parameters in the routes?
Pretty awesome, right!
You will never have to make any check related to the request parameters. All these checks will be transported to this single middleware.
What is a middleware?
Middleware is a piece of code that is used to make changes to request or response data.
Let’s start by writing a simple request validation middleware.
So this is the simple structure of middleware in Node.js. There are three types of checks that are going on in there.
Required Parameter check
This check tries to find if the parameter being requested is required or not. We can specify this while specifying the schema of the route parameters. I will share this schema a little later in this tutorial. If the parameter is required for the route and is not present in the parameters of the route, it will simply raise 400.
This can also give a custom message specifying which param exactly is not present in the request parameters. This part is described on line 21
.
Type check
JavaScript being less strict related to the type of the variables, we want to add a check which will try to check if the type specified in the schema of the route parameters is the same as the type received from the request parameters.
This part of the code is written on line 6
.
Other validation checks
There are multiple occasions when you want to add your own validations to request parameters. For example, you don’t want the value to be equal to 0. You can simply create the function and pass it in the schema of the route parameters. These checks are written on line 13
.
Here is the schema for route parameters.
This is how you will create a route in your NodeJS
app.
The cool thing about this is you can at any time integrate your own checks into this and you don’t have to worry about the error messages passed to frontend. They are handled as well.
Simple Validator Required Parameter check
Following error is produced when the required parameter is not present in the request body.
Simple Validator Length check
The following error is produced when the length of the required parameter is not accurate.
Simple Validator Wrong type check
The following error is produced when the type of the required parameter is not accurate.
Checking the performance of Simple validator
The above validation is trying to apply three checks.
- Given param,
abc
must be a present. - Given param,
abc
must be a String. - Given param,
abc
must be of length 10.
I will use all three of these scenarios and try to find out the time taken to raise the error and give back the response.
How am I checking the performance?
Javascript provides a very clean way to find out the time taken from one point to the next point. All you have to do is write console.time('')
at the place where you want to start and console.timeEnd('')
where you want to stop.
Passed string will be used as a reference, for example, I am using console.time('start')
and console.timeEnd('start')
. i.e. the string passed should be the same, in both the cases.
I added them to the middleware code.
const validateParams = function (requestParams) {
return function (req, res, next) {
console.time('start');
...
if (!checkParamType(reqParam, param)) {
console.timeEnd('start');
} else {
if (!runValidators(reqParam, param)) {
console.timeEnd('start');
...
} else if (param.required){
console.timeEnd('start');
...
}
};
Finally, I ran the APIs for each error type separately for 5 times and took their average. These were the final scores.
No parameter present in the body: 0.3228ms
Wrong length of the parameter: 0.7242ms
Wrong type of the parameter: 1.131ms( Used integer)
Writing tests for simple request parameter validation middleware
One of my colleagues asked me to write tests for this framework as this was going to be used at a lot of places and I agreed with him. But I was a little skeptical on how can we test this framework. After some Googling and StackOverflowing, I was able to test this framework. Here is the code for this.
Using Joi as an alternative to adding request parameter validator
I later found that you can use Joi for adding validations to parameters. This is a good option and you can use it if you want.
They provide a lot of validations out of the box which are easy to plug in.
Of course, you will have to install Joi to use it.
Your 50
line of middleware code will just reduce to 26
lines.
const Joi = require('joi');
const lodash = require('lodash');
const validateParams = function (paramSchema) {
return async (req, res, next) => {
const schema = Joi.object().keys(paramSchema);
const paramSchemaKeys = Object.keys(paramSchema);
let requestParamObj = {};
for (let key of paramSchemaKeys){
requestParamObj[key] = lodash.get(req.body, key);
}
try{
await Joi.validate(requestParamObj, schema);
} catch (err) {
return res.send(400, {
status: 400,
result: err.details[0].message
});
}
next();
}
};
module.exports = {
validateParams: validateParams
};
The main thing to look at here is the
await Joi.validate(requestParamObj, schema);
Similarly, you will have to set up your route
as follows.
router.post('/abc', validateParams({
abc: Joi.string().length(10).required(),
}), routeFunction);
Pretty clean right, I like this method more than creating something new of my own. But you will have to keep in mind that this will increase your bundle size. This is something that you have to think on your own and make a decision.
Joi Required Parameter check
The following error is produced when the required parameter is not present in the request body.
Joi Length check
The following error is produced when the length of the required parameter is not accurate.
Joi Wrong type check
The following error is produced when the type of the required parameter is not accurate.
Validator Success response
Successful response.
Checking the performance of JOI validator
I am using the same setup as I was using while checking the performance of Simple validator. Following are the results.
No parameter present in the body: 1.0778ms
Wrong length of the parameter: 5.6514ms
Wrong type of the parameter: 14.3162ms( Used integer)
I will use the same formula in the other libraries as well.
Using ajv as an alternative to adding request parameter validator
Then, we tried ajv
for validating our requests.
Why ajv
?
ajv
uses the JSON
schema validation which is kind of a standard in any language, and we can keep using it, even if we move to another language in the future.
Also, they consider themselves as the fastest JSON schema validator out there and speed is something that we care for a lot.
Install ajv using the following command.
Finally, write the ajv validation middleware as follows.
const Ajv = require('ajv');
const lodash = require('lodash');
const validateParams = function (paramSchema) {
return async (req, res, next) => {
const ajv = new Ajv({$data: true});
const paramSchemaKeys = Object.keys(paramSchema.properties);
let requestParamObj = {};
for (let key of paramSchemaKeys){
// Use req.query/req.params if you want to validate query params.
requestParamObj[key] = lodash.get(req.body, key);
}
const validated = ajv.validate(paramSchema, requestParamObj);
if (!validated) {
return res.send(400, {
status: 400,
result: getCustomMessage(ajv.errors[0])
})
}
next();
}
};
const getCustomMessage = (errorObject) => {
if (['minLength', 'maxLength'].includes(errorObject.keyword)) {
return `${errorObject.dataPath.replace('.', '')} ${errorObject.message}`;
}
return errorObject.message;
};
module.exports = {
validateParams: validateParams
};
All you have to worry about is the ajv.validate
function, which is used to carry out the validations. Unlike Joi, this validate
method is not async.
The validation definition method is a little different for ajv
.
router.post('/abc', validateParams({
properties: {
abc: {
type: 'string',
maxLength: 10,
minLength: 10
},
},
required: ['abc']
}), routeFunction);
AJV Required Parameter check
Following error is produced when required parameter is not present in the request body.
AJV length check
Following error is produced when the length of the required parameter is not accurate.
AJV Wrong type check
Following error is produced when the type of the required parameter is not accurate.
Checking the performance of AJV validator
I am using the same setup as I was using while checking the performance of Simple validator.
No parameter present in the body: 97.44ms
Wrong length of the parameter: 62.88ms
Wrong type of the parameter: 82.227ms( Used integer)
According to the above numbers, it is pretty clear that middleware using a Simple validator does perform better by big margins. You can choose whichever you want to use.
Edit 1: Using Express-Validation for the validation
Express-Validation is a package build on the top of Joi and can be used to validate your request parameters fairly easily. You don’t have to worry about this writing a separate middleware.
Install the package using the following command.
npm i express-validation --save
Adding the middleware is as simple as the following code.
const express = require('express')
const bodyParser = require('body-parser')
const { validate, ValidationError, Joi } = require('express-validation')
const loginValidation = {
body: Joi.object({
email: Joi.string()
.email()
.required(),
password: Joi.string()
.regex(/[a-zA-Z0-9]{3,30}/)
.required(),
}),
}
const app = express();
app.use(bodyParser.json())
app.post('/login', validate(loginValidation, {}, {}), (req, res) => {
res.json(200)
})
app.use(function(err, req, res, next) {
if (err instanceof ValidationError) {
return res.status(err.statusCode).json(err)
}
return res.status(500).json(err)
})
app.listen(3000)
I hope you guys will like the idea behind the post. Please share it with your colleagues and let me know on social media platforms.
I am also open to other standards that are followed in the market. Please leave your ideas in the comments.
This guide covers mapping the request query string parameters to Spring Controller method arguments using @RequestParam annotation.
Overview
Spring Web provides the @RequestParam annotation for extracting and mapping query string parameters of a request into Spring Controller’s method arguments. Without that, we’ll have to pull the parameter values manually from the HttpServletRequest and cast them to variables of the desired types.
Spring, along with the annotation, saves us from manual work. It also provides flexible ways of reading single-value, multi-value, or optional parameters and makes them available as method arguments.
Before we see the @RequestParam in action, we will look at the annotation itself.
Spring’s @RequestParam annotation maps request parameters to the arguments of a request handler method in Spring Controllers. We can use this annotation on method parameters to bind request parameters by their names.
Interestingly, this annotation is available in Spring Web and Spring WebFlux annotation-based controllers.
Code language: Java (java)
@Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequestParam { @AliasFor("name") String value() default ""; @AliasFor("value") String name() default ""; boolean required() default true; String defaultValue() default "nttnttnue000ue001ue002nttttn"; }
There are only three attributes in this annotation. The name attribute denotes the name of the request param, and we can omit it if the method and request parameters have the same name. The required attribute represents if the request parameter is mandatory, which is true by default. Lastly, the default attribute can specify a fall-back value of a request parameter, which will be used only when the request parameter isn’t present.
Using @RequestParam
To see the @RequestParam in action, we’ll write a @GetMapping method in a Spring @RestController and execute the endpoint with requests contenting query parameters.
Reading Single Query String Parameter
We want to read a single query string parameter from a request and map it to the controller method argument.
@GetMapping("/data1") public String singleParam(@RequestParam String id) { return "id: " + id; }
Code language: Java (java)
Behind the scenes, Spring looks for a query string parameter ‘id‘ and extracts its value from the request. Then it invokes the request handler singleParam(Id) and passes the param value as an argument.
To test this controller, let’s execute the GET /data endpoint with the id request parameter.
Code language: Bash (bash)
~ curl 'http://localhost:8080/data1?id=112' -- id: 112
Reading Multiple Query String Parameters
Similarly, if the request has more than one query string parameter, we can use @RequestParam annotation individually on the respective method arguments.
Code language: Java (java)
@GetMapping("/data2") public String multiParam( @RequestParam String id, @RequestParam String name) { return "id: " + id + ", name: " + name; }
Our controller reads the id and name parameters from the request.
Code language: Bash (bash)
~ curl 'http://localhost:8080/data2?id=112&name=Jon' -- id: 112, name: Jon
Executing the respective GET request, we see that both request parameters are mapped correctly.
Mapping Query String Parameters with Specific Type
By default, all the request query string parameters are represented as String. However, our method argument can describe the variable to a specific data type. Interestingly, Spring takes care of this type-casting internally.
For example, our Spring Controller reads a request parameter as a Long value.
Code language: Java (java)
@GetMapping("/data3") public String typedParam(@RequestParam Long id) { return "id: " + id; }
If Spring cannot cast the param value, it returns HTTP Status 400 (BAD_REQUEST), as shown.
Code language: Bash (bash)
~ curl -i 'http://localhost:8080/data3?id=abc' -- HTTP/1.1 400 ...
Reading MultiValue Query String Parameters
Any query string parameter in a request can have multiple values. That means they can appear multiple times in the URL or have comma-separated values. Spring @RequestParam annotation maps such request parameters to a collection- for example, List.
Code language: Java (java)
@GetMapping("/data4") public String multiValueParams(@RequestParam List<String> id) { return "id: " + id; }
Alternatively, we can also use an Array type or a Set type. Using the parameter of Set type ensures that the query string has unique values.
Let’s execute a request with a comma-separated multi-value parameter.
Code language: Bash (bash)
~ curl 'http://localhost:8080/data4?id=12,13,15,16' -- id: [12, 13, 15, 16]
Or a request having the same parameter multiple times with different values.
~ curl 'http://localhost:8080/data4?id=12&id=13&id=15' -- id: [12, 13, 15]
Code language: Bash (bash)
Making Query String Parameters Optional
By default, all the request parameters annotated with the @RequestParam are mandatory. Thus in the previous examples, if we skip a query string parameter from a request, we get a Bad Request response.
Code language: Bash (bash)
~ curl -i 'http://localhost:8080/data4' -- HTTP/1.1 400 ...
Now, let’s see the three ways of using @RequestParam annotation on optional request parameters.
Using @RequestParam required=false
To support optional query string parameters in a Spring controller, we can use the @RequestParam with the required=false flag.
Code language: Java (java)
@GetMapping("/data5") public String optionalParams (@RequestParam(required = false) Long id) { return "id: " + id; }
Now, we can omit the id parameter from our request.
Code language: JavaScript (javascript)
~ curl 'http://localhost:8080/data5' -- id: null
Using @RequestParam with Java Optional
Alternatively, we can use the Java Optional to make a particular query string parameter in a request optional. We can also provide a default value for a missing query string using Java Optional.
@GetMapping("/data6") public String javaOptionalParams (@RequestParam Optional<String> id) { return "id: " + id.orElseGet(() -> "Unknown"); }
Code language: Java (java)
The example shows how to use Java Optional to make a Spring Controller request parameter not mandatory.
Code language: Bash (bash)
~ curl 'http://localhost:8080/data6' -- id: Unknown
Using @RequestParam defaultValue
Lastly, we can use the defaultValue attribute of the @RequestParam annotation to specify a default value of a request parameter. Spring uses the provided defaultValue only when the actual parameter in the request is absent or empty.
Code language: Java (java)
@GetMapping("/data7") public String defaultParams (@RequestParam(defaultValue = "Unknown") String id) { return "id: " + id; }
Let’s test this by executing a GET request without any parameters.
Code language: Bash (bash)
~ curl 'http://localhost:8080/data7' -- id: Unknown
Mapping Query String Parameters by Name
In the previous examples, the controller method argument and a request’s respective query string parameter had the same name. Thankfully, we can use the name attribute of Spring @RequestParam when the query string parameter name is different than that of the method argument.
Code language: Java (java)
@GetMapping("/data8") public String namedParams (@RequestParam(name = "id") String dataId) { return "dataId: " + dataId; }
Reading Query String Parameters as Java Map
With the Spring @RequestParam annotation, we can collect all the request query string parameters as a HashMap. That is useful when we want to read all the query string parameters and their values together.
Code language: Java (java)
@GetMapping("/data9") public String mappedParams (@RequestParam Map<String, String> dataQuery) { return dataQuery.toString(); }
Code language: Bash (bash)
~ curl 'http://localhost:8080/data9?id=12&year=2034' -- {id=12, year=2034}
Summary
We have covered using Spring @RequestParam annotation to map query string parameters to controller method arguments. This annotation is available for both Spring MVC and Spring WebFlux.
In this tutorial, we had a detailed overview of the @RequestParam annotation and covered various scenarios of mapping request parameters to controller method arguments.
Please refer to our GitHub Repository for the complete source code of the examples used here.
Related Posts:
- Spring @PathVariable Examples
- Return Specific HTTP Response Status in Spring
- Read HTTP Headers in Spring REST Controller
- Custom Error Messages in Spring REST API
- Custom Media Types in Spring REST API
Прежде чем начать, убедитесь, что установлена последняя версия Requests.
Для начала, давайте рассмотрим простые примеры.
Импортируйте модуль Requests:
Попробуем получить веб-страницу с помощью get-запроса. В этом примере давайте рассмотрим общий тайм-лайн GitHub:
r = requests.get('https://api.github.com/events')
Мы получили объект Response
с именем r
. С помощью этого объекта можно получить всю необходимую информацию.
Простой API Requests означает, что все типы HTTP запросов очевидны. Ниже приведен пример того, как вы можете сделать POST запрос:
r = requests.post('https://httpbin.org/post', data = {'key':'value'})
Другие типы HTTP запросов, такие как : PUT, DELETE, HEAD и OPTIONS так же очень легко выполнить:
r = requests.put('https://httpbin.org/put', data = {'key':'value'})
r = requests.delete('https://httpbin.org/delete')
r = requests.head('https://httpbin.org/get')
r = requests.options('https://httpbin.org/get')
Передача параметров в URL
Часто вам может понадобится отправить какие-то данные в строке запроса URL. Если вы настраиваете URL вручную, эти данные будут представлены в нем в виде пар ключ/значение после знака вопроса. Например, httpbin.org/get?key=val
. Requests позволяет передать эти аргументы в качестве словаря, используя аргумент params
. Если вы хотите передать key1=value1
и key2=value2
ресурсу httpbin.org/get
, вы должны использовать следующий код:
payload = {'key1': 'value1', 'key2': 'value2'}
r = requests.get('https://httpbin.org/get', params=payload)
print(r.url)
Как видно, URL был сформирован правильно:
https://httpbin.org/get?key2=value2&key1=value1
Ключ словаря, значение которого None
, не будет добавлен в строке запроса URL.
Вы можете передать список параметров в качестве значения:
>>> payload = {'key1': 'value1', 'key2': ['value2', 'value3']}
>>> r = requests.get('https://httpbin.org/get', params=payload)
>>> print(r.url)
https://httpbin.org/get?key1=value1&key2=value2&key2=value3
Содержимое ответа (response)
Мы можем прочитать содержимое ответа сервера. Рассмотрим снова тайм-лайн GitHub:
>>> import requests
>>> r = requests.get('https://api.github.com/events')
>>> r.text
'[{"repository":{"open_issues":0,"url":"https://github.com/...
Requests будет автоматически декодировать содержимое ответа сервера. Большинство кодировок unicode
декодируются без проблем.
Когда вы делаете запрос, Requests делает предположение о кодировке, основанное на заголовках HTTP. Эта же кодировка текста, используется при обращение к r.text
. Можно узнать, какую кодировку использует Requests, и изменить её с помощью r.encoding
:
>>> r.encoding
'utf-8'
>>> r.encoding = 'ISO-8859-1'
Если вы измените кодировку, Requests будет использовать новое значение r.encoding
всякий раз, когда вы будете использовать r.text
. Вы можете сделать это в любой ситуации, где нужна более специализированная логика работы с кодировкой содержимого ответа.
Например, в HTML
и XML
есть возможность задавать кодировку прямо в теле документа. В подобных ситуациях вы должны использовать r.content
, чтобы найти кодировку, а затем установить r.encoding
. Это позволит вам использовать r.text
с правильной кодировкой.
Requests может также использовать пользовательские кодировки в случае, если в них есть потребность. Если вы создали свою собственную кодировку и зарегистрировали ее в модуле codecs, используйте имя кодека в качестве значения r.encoding
.
Бинарное содержимое ответа
Вы можете также получить доступ к телу ответа в виде байтов для не текстовых ответов:
>>> r.content
b'[{"repository":{"open_issues":0,"url":"https://github.com/...
Передача со сжатием gzip
и deflate
автоматически декодируются для вас.
Например, чтобы создать изображение на основе бинарных данных, возвращаемых при ответе на запрос, используйте следующий код:
from PIL import Image
from io import BytesIO
i = Image.open(BytesIO(r.content))
Содержимое ответа в JSON
Если вы работаете с данными в формате JSON, воспользуйтесь встроенным JSON декодером:
>>> import requests
>>> r = requests.get('https://api.github.com/events')
>>> r.json()
[{'repository': {'open_issues': 0, 'url': 'https://github.com/...
Если декодирование в JSON не удалось, r.json()
вернет исключение. Например, если ответ с кодом 204 (No Content), или на случай если ответ содержит не валидный JSON, попытка обращения к r.json()
будет возвращать ValueError: No JSON object could be decoded
.
Следует отметить, что успешный вызов r.json()
не указывает на успешный ответ сервера. Некоторые серверы могут возвращать объект JSON при неудачном ответе (например, сведения об ошибке HTTP 500). Такой JSON будет декодирован и возвращен. Для того, чтобы проверить успешен ли запрос, используйте r.raise_for_status()
или проверьте какой r.status_code
.
Необработанное содержимое ответа
В тех редких случаях, когда вы хотите получить доступ к “сырому” ответу сервера на уровне сокета, обратитесь к r.raw
. Если вы хотите сделать это, убедитесь, что вы указали stream=True
в вашем первом запросе. После этого вы уже можете проделать следующее:
>>> r = requests.get('https://api.github.com/events', stream=True)
>>> r.raw
>>> r.raw.read(10)
'x1fx8bx08x00x00x00x00x00x00x03'
Однако, можно использовать подобный код как шаблон, чтобы сохранить результат в файл:
with open(filename, 'wb') as fd:
for chunk in r.iter_content(chunk_size=128):
fd.write(chunk)
Использование r.iter_content
обработает многое из того, с чем бы вам пришлось иметь дело при использовании r.raw
напрямую. Для извлечения содержимого при потоковой загрузке, используйте способ, описанный выше. Обратите внимание, что chunk_size
можно свободно скорректировать до числа, которое лучше подходит в вашем случае.
Важное замечание об использовании
Response.iter_content
иResponse.raw
.Response.iter_content
будет автоматически декодироватьgzip
иdeflate
.Response.raw
— необработанный поток байтов, он не меняет содержимое ответа. Если вам действительно нужен доступ к байтам по мере их возврата, используйтеResponse.raw
.
Пользовательские заголовки
Если вы хотите добавить HTTP заголовки в запрос, просто передайте соответствующий dict
в параметре headers
.
Например, мы не указали наш user-agent
в предыдущем примере:
url = 'https://api.github.com/some/endpoint'
headers = {'user-agent': 'my-app/0.0.1'}
r = requests.get(url, headers=headers)
Заголовкам дается меньший приоритет, чем более конкретным источникам информации. Например:
- Заголовки авторизации, установленные с помощью
headers=
будут переопределены, если учетные данные указаны.netrc
, которые, в свою очередь переопределены параметромauth=
. - Они же будут удалены при редиректе.
- Заголовки авторизации с прокси будут переопределены учетными данными прокси-сервера, которые указаны в вашем URL.
- Content-Length будут переопределены, когда вы определите длину содержимого.
Кроме того, запросы не меняют свое поведение вообще, основываясь на указанных пользовательских заголовках.
Значения заголовка должны быть
string, bytestring или unicode
. Хотя это разрешено, рекомендуется избегать передачи значений заголовковunicode
.
Более сложные POST запросы
Часто вы хотите послать некоторые form-encoded
данные таким же образом, как это делается в HTML форме. Для этого просто передайте соответствующий словарь в аргументе data
. Ваш словарь данных в таком случае будет автоматически закодирован как HTML форма, когда будет сделан запрос:
>>> payload = {'key1': 'value1', 'key2': 'value2'}
>>> r = requests.post("https://httpbin.org/post", data=payload)
>>> print(r.text)
{
...
"form": {
"key2": "value2",
"key1": "value1"
},
...
}
Аргумент data
также может иметь несколько значений для каждого ключа. Это можно сделать, указав data
в формате tuple
, либо в виде словаря со списками в качестве значений. Особенно полезно, когда форма имеет несколько элементов, которые используют один и тот же ключ:
>>> payload_tuples = [('key1', 'value1'), ('key1', 'value2')]
>>> r1 = requests.post('https://httpbin.org/post', data=payload_tuples)
>>> payload_dict = {'key1': ['value1', 'value2']}
>>> r2 = requests.post('https://httpbin.org/post', data=payload_dict)
>>> print(r1.text)
{
...
"form": {
"key1": [
"value1",
"value2"
]
},
...
}
>>> r1.text == r2.text
True
Бывают случаи, когда нужно отправить данные не закодированные методом form-encoded
. Если вы передадите в запрос строку вместо словаря, эти данные отправятся в не измененном виде.
К примеру, GitHub API v3 принимает закодированные JSON POST/PATCH данные:
import json
url = 'https://api.github.com/some/endpoint'
payload = {'some': 'data'}
r = requests.post(url, data=json.dumps(payload))
Вместо того, чтобы кодировать dict
, вы можете передать его напрямую, используя параметр json
(добавленный в версии 2.4.2), и он будет автоматически закодирован:
url = 'https://api.github.com/some/endpoint'
payload = {'some': 'data'}
r = requests.post(url, json=payload)
Обратите внимание, параметр json
игнорируется, если передаются data
или files
.
Использование параметра json
в запросе изменит заголовок Content-Type на application/json
.
POST отправка Multipart-Encoded файла
Запросы упрощают загрузку файлов с многостраничным кодированием (Multipart-Encoded) :
>>> url = 'https://httpbin.org/post'
>>> files = {'file': open('report.xls', 'rb')}
>>> r = requests.post(url, files=files)
>>> r.text
{
...
"files": {
"file": ""
},
...
}
Вы можете установить имя файла, content_type и заголовки в явном виде:
>>> url = 'https://httpbin.org/post'
>>> files = {'file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel', {'Expires': '0'})}
>>> r = requests.post(url, files=files)
>>> r.text
{
...
"files": {
"file": ""
},
...
}
Можете отправить строки, которые будут приняты в виде файлов:
>>> url = 'https://httpbin.org/post'
>>> files = {'file': ('report.csv', 'some,data,to,sendnanother,row,to,sendn')}
>>> r = requests.post(url, files=files)
>>> r.text
{
...
"files": {
"file": "some,data,to,send\nanother,row,to,send\n"
},
...
}
В случае, если вы отправляете очень большой файл как запрос multipart/form-data, возможно понадобиться отправить запрос потоком. По умолчанию, requests не поддерживает этого, но есть отдельный пакет, который это делает — requests-toolbelt
. Ознакомьтесь с документацией toolbelt для получения более детальной информации о том, как им пользоваться.
Для отправки нескольких файлов в одном запросе, обратитесь к расширенной документации.
Предупреждение!
Настоятельно рекомендуется открывать файлы в бинарном режиме. Это связано с тем, что запросы могут пытаться предоставить для вас заголовок Content-Length
, и если это значение будет установлено на количество байтов в файле будут возникать ошибки, при открытии файла в текстовом режиме.
Коды состояния ответа
Мы можем проверить код состояния ответа:
>>> r = requests.get('https://httpbin.org/get')
>>> r.status_code
200
У requests есть встроенный объект вывода кодов состояния:
>>> r.status_code == requests.codes.ok
True
Если мы сделали неудачный запрос (ошибка 4XX или 5XX), то можем вызвать исключение с помощью r.raise_for_status()
:
>>> bad_r = requests.get('https://httpbin.org/status/404')
>>> bad_r.status_code
404
>>> bad_r.raise_for_status()
Traceback (most recent call last):
File "requests/models.py", line 832, in raise_for_status
raise http_error
requests.exceptions.HTTPError: 404 Client Error
Но если status_code
для r
оказался 200, то когда мы вызываем raise_for_status()
мы получаем:
>>> r.raise_for_status()
None
Заголовки ответов
Мы можем просматривать заголовки ответа сервера, используя словарь Python:
>>> r.headers
{
'content-encoding': 'gzip',
'transfer-encoding': 'chunked',
'connection': 'close',
'server': 'nginx/1.0.4',
'x-runtime': '148ms',
'etag': '"e1ca502697e5c9317743dc078f67693f"',
'content-type': 'application/json'
}
Это словарь особого рода, он создан специально для HTTP заголовков. Согласно с RFC 7230, имена заголовков HTTP нечувствительны к регистру.
Теперь мы можем получить доступ к заголовкам с большим буквами или без, если захотим:
>>> r.headers['Content-Type']
'application/json'
>>> r.headers.get('content-type')
'application/json'
Cookies
Если в запросе есть cookies, вы сможете быстро получить к ним доступ:
>>> url = 'https://example.com/some/cookie/setting/url'
>>> r = requests.get(url)
>>> r.cookies['example_cookie_name']
'example_cookie_value'
Чтобы отправить собственные cookies на сервер, используйте параметр cookies:
>>> url = 'https://httpbin.org/cookies'
>>> cookies = dict(cookies_are='working')
>>> r = requests.get(url, cookies=cookies)
>>> r.text
'{"cookies": {"cookies_are": "working"}}'
Cookies возвращаются в RequestsCookieJar
, который работает как dict
, но также предлагает более полный интерфейс, подходящий для использования в нескольких доменах или путях. Словарь с cookie может также передаваться в запросы:
>>> jar = requests.cookies.RequestsCookieJar()
>>> jar.set('tasty_cookie', 'yum', domain='httpbin.org', path='/cookies')
>>> jar.set('gross_cookie', 'blech', domain='httpbin.org', path='/elsewhere')
>>> url = 'https://httpbin.org/cookies'
>>> r = requests.get(url, cookies=jar)
>>> r.text
'{"cookies": {"tasty_cookie": "yum"}}'
Редиректы и история
По умолчанию Requests будет выполнять редиректы для всех HTTP глаголов, кроме HEAD.
Мы можем использовать свойство history
объекта Response, чтобы отслеживать редиректы .
Список Response.history содержит объекты Response, которые были созданы для того, чтобы выполнить запрос. Список сортируется от более ранних, до более поздних ответов.
Например, GitHub перенаправляет все запросы HTTP на HTTPS:
>>> r = requests.get('https://github.com/')
>>> r.url
'https://github.com/'
>>> r.status_code
200
>>> r.history
[]
Если вы используете запросы GET, OPTIONS, POST, PUT, PATCH или DELETE, вы можете отключить обработку редиректа с помощью параметра allow_redirects
:
>>> r = requests.get('https://github.com/', allow_redirects=False)
>>> r.status_code
301
>>> r.history
[]
Если вы используете HEAD, вы также можете включить редирект:
>>> r = requests.head('https://github.com/', allow_redirects=True)
>>> r.url
'https://github.com/'
>>> r.history
[]
Тайм-ауты
Вы можете сделать так, чтобы Requests прекратил ожидание ответа после определенного количества секунд с помощью параметра timeout
.
Почти весь код должен использовать этот параметр в запросах. Несоблюдение этого требования может привести к зависанию вашей программы:
>>> requests.get('https://github.com/', timeout=0.001)
Traceback (most recent call last):
File "", line 1, in
requests.exceptions.Timeout: HTTPConnectionPool(host='github.com', port=80): Request timed out. (timeout=0.001)
Timeout это не ограничение по времени полной загрузки ответа. Исключение возникает, если сервер не дал ответ за timeout секунд (точнее, если ни одного байта не было получено от основного сокета за timeout секунд).
Ошибки и исключения
В случае неполадок в сети (например, отказа DNS, отказа соединения и т.д.), Requests вызовет исключение ConnectionError
.
Response.raise_for_status()
вызовет HTTPError
если в запросе HTTP возникнет статус код ошибки.
Если выйдет время запроса, вызывается исключение Timeout
. Если запрос превышает заданное значение максимального количества редиректов, то вызывают исключение TooManyRedirects
.
Все исключения, которые вызывает непосредственно Requests унаследованы от requests.exceptions.RequestException
.
Тест на знание основ Requests
Какой из HTTP-запросов является правильным?
requests.post(url, data={‘key’:’value’})
requests.get(url, params={‘key’:’value’})
requests.post(url, params={‘key’:’value’})
Какое из утверждений верно?
response.json() возвращает содержимое ответа в виде объекта dict
response.text возвращает содержимое ответа в виде байтового объекта
response.content возвращает содержимое ответа в виде строкового объекта
response.json возвращает содержимое ответа в виде объекта dict
Как получить куки из ответа на запрос?
Что вернет метод status_code объекта Response?
Ошибку, если запрос неудачный
Какой код сформирует url «https://test.com/page?key1=value1&key2=value21,value22»
r.get(‘https://test.com/page’, params={‘key1’: ‘value1’, ‘key2’: [‘value21’, ‘value22’]})
r.get(‘https://test.com/page’, params={‘key1’: ‘value1’, ‘key2’: ‘value21,value22’})
r.get(‘https://test.com/page’, params={‘key1’: ‘value1’, ‘key2’: ‘value21’, ‘key2’: ‘value22’})
Библиотека requests
является стандартным инструментом для составления HTTP-запросов в Python. Простой и аккуратный API значительно облегчает трудоемкий процесс создания запросов. Таким образом, можно сосредоточиться на взаимодействии со службами и использовании данных в приложении.
Содержание статьи
- Python установка библиотеки requests
- Python библиотека Requests метод GET
- Объект Response получение ответа на запрос в Python
- HTTP коды состояний
- Получить содержимое страницы в Requests
- HTTP заголовки в Requests
- Python Requests параметры запроса
- Настройка HTTP заголовка запроса (headers)
- Примеры HTTP методов в Requests
- Python Requests тело сообщения
- Python Requests анализ запроса
- Python Requests аутентификация HTTP AUTH
- Python Requests проверка SSL сертификата
- Python Requests производительность приложений
- Объект Session в Requests
- HTTPAdapter — Максимальное количество повторов запроса в Requests
В данной статье представлены наиболее полезные особенности requests. Показано, как изменить и приспособить requests к различным ситуациям, с которыми программисты сталкиваются чаще всего. Здесь также даются советы по эффективному использованию requests и предотвращению влияния сторонних служб, которые могут сильно замедлить работу используемого приложения. Мы использовали библиотек requests в уроке по парсингу html через библиотеку BeautifulSoup.
Ключевые аспекты инструкции:
- Создание запросов при помощи самых популярных HTTP методов;
- Редактирование заголовков запросов и данных при помощи строки запроса и содержимого сообщения;
- Анализ данных запросов и откликов;
- Создание авторизированных запросов;
- Настройка запросов для предотвращения сбоев и замедления работы приложения.
В статье собран оптимальный набор информации, необходимый для понимания данных примеров и особенностей их использования. Информация представлена в доступной в форме. Тем не менее, стоит иметь в виду, что для оптимального разбора инструкции потребуются хотя бы базовые знания HTTP.
Далее будут показаны наиболее эффективные методы использования requests
в разрабатываемом приложении.
Python установка библиотеки requests
Для начала работы потребуется установить библиотеку requests
. Для этого используется следующая команда.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Telegram Чат & Канал
Вступите в наш дружный чат по Python и начните общение с единомышленниками! Станьте частью большого сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Тем, кто для работы с пакетами Python, использует виртуальную среду Pipenv, необходимо использовать немного другую команду.
$ pipenv install requests |
Сразу после установки requests
можно полноценно использовать в приложении. Импорт requests
производится следующим образом.
Таким образом, все подготовительные этапы для последующего использования requests
завершены. Начинать изучение requests лучше всего с того, как сделать запрос GET
.
Python библиотека Requests метод GET
Такие HTTP методы, как GET
и POST
, определяют, какие действия будут выполнены при создании HTTP запроса. Помимо GET
и POST
для этой задачи могут быть использованы некоторые другие методы. Далее они также будут описаны в руководстве.
GET
является одним из самых популярных HTTP методов. Метод GET
указывает на то, что происходит попытка извлечь данные из определенного ресурса. Для того, чтобы выполнить запрос GET
, используется requests.get()
.
Для проверки работы команды будет выполнен запрос GET
в отношении Root REST API на GitHub. Для указанного ниже URL вызывается метод get()
.
requests.get(‘https://api.github.com’) <Response [200]> |
Если никакие python ошибки не возникло, вас можно поздравить – первый запрос успешно выполнен. Далее будет рассмотрен ответ на данный запрос, который можно получить при помощи объекта Response.
Объект Response получение ответа на запрос в Python
Response
представляет собой довольно мощный объект для анализа результатов запроса. В качестве примера будет использован предыдущий запрос, только на этот раз результат будет представлен в виде переменной. Таким образом, получится лучше изучить его атрибуты и особенности использования.
response = requests.get(‘https://api.github.com’) |
В данном примере при помощи get()
захватывается определенное значение, что является частью объекта Response
, и помещается в переменную под названием response
. Теперь можно использовать переменную response
для того, чтобы изучить данные, которые были получены в результате запроса GET
.
HTTP коды состояний
Самыми первыми данными, которые будут получены через Response
, будут коды состояния. Коды состояния сообщают о статусе запроса.
Например, статус 200 OK
значит, что запрос успешно выполнен. А вот статус 404 NOT FOUND
говорит о том, что запрашиваемый ресурс не был найден. Существует множество других статусных кодов, которые могут сообщить важную информацию, связанную с запросом.
Используя .status_code
, можно увидеть код состояния, который возвращается с сервера.
>>> response.status_code 200 |
.status_code
вернул значение 200
. Это значит, что запрос был выполнен успешно, а сервер ответил, отобразив запрашиваемую информацию.
В некоторых случаях необходимо использовать полученную информацию для написания программного кода.
if response.status_code == 200: print(‘Success!’) elif response.status_code == 404: print(‘Not Found.’) |
В таком случае, если с сервера будет получен код состояния 200
, тогда программа выведет значение Success!
. Однако, если от сервера поступит код 404
, тогда программа выведет значение Not Found
.
requests
может значительно упростить весь процесс. Если использовать Response
в условных конструкциях, то при получении кода состояния в промежутке от 200
до 400
, будет выведено значение True
. В противном случае отобразится значение False
.
Последний пример можно упростить при помощи использования оператора if
.
if response: print(‘Success!’) else: print(‘An error has occurred.’) |
Стоит иметь в виду, что данный способ не проверяет, имеет ли статусный код точное значение 200
. Причина заключается в том, что другие коды в промежутке от 200
до 400
, например, 204 NO CONTENT
и 304 NOT MODIFIED
, также считаются успешными в случае, если они могут предоставить действительный ответ.
К примеру, код состояния 204
говорит о том, что ответ успешно получен, однако в полученном объекте нет содержимого. Можно сказать, что для оптимально эффективного использования способа необходимо убедиться, что начальный запрос был успешно выполнен. Требуется изучить код состояния и в случае необходимости произвести необходимые поправки, которые будут зависеть от значения полученного кода.
Допустим, если при использовании оператора if
вы не хотите проверять код состояния, можно расширить диапазон исключений для неудачных результатов запроса. Это можно сделать при помощи использования .raise_for_status()
.
import requests from requests.exceptions import HTTPError for url in [‘https://api.github.com’, ‘https://api.github.com/invalid’]: try: response = requests.get(url) # если ответ успешен, исключения задействованы не будут response.raise_for_status() except HTTPError as http_err: print(f‘HTTP error occurred: {http_err}’) # Python 3.6 except Exception as err: print(f‘Other error occurred: {err}’) # Python 3.6 else: print(‘Success!’) |
В случае вызова исключений через .raise_for_status()
к некоторым кодам состояния применяется HTTPError
. Когда код состояния показывает, что запрос успешно выполнен, программа продолжает работу без применения политики исключений.
На заметку. Для более продуктивной работы в Python 3.6 будет не лишним изучить f-строки. Не стоит пренебрегать ими, так как это отличный способ упростить форматирование строк.
Анализ способов использования кодов состояния, полученных с сервера, является неплохим стартом для изучения requests. Тем не менее, при создании запроса GET, значение кода состояния является не самой важной информацией, которую хочет получить программист. Обычно запрос производится для извлечения более содержательной информации. В дальнейшем будет показано, как добраться до актуальных данных, которые сервер высылает отправителю в ответ на запрос.
Зачастую ответ на запрос GET
содержит весьма ценную информацию. Она находится в теле сообщения и называется пейлоад (payload). Используя атрибуты и методы библиотеки Response
, можно получить пейлоад в различных форматах.
Для того, чтобы получить содержимое запроса в байтах, необходимо использовать .content
.
>>> response = requests.get(‘https://api.github.com’) >>> response.content b‘{«current_user_url»:»https://api.github.com/user»,»current_user_authorizations_html_url»:»https://github.com/settings/connections/applications{/client_id}»,»authorizations_url»:»https://api.github.com/authorizations»,»code_search_url»:»https://api.github.com/search/code?q={query}{&page,per_page,sort,order}»,»commit_search_url»:»https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}»,»emails_url»:»https://api.github.com/user/emails»,»emojis_url»:»https://api.github.com/emojis»,»events_url»:»https://api.github.com/events»,»feeds_url»:»https://api.github.com/feeds»,»followers_url»:»https://api.github.com/user/followers»,»following_url»:»https://api.github.com/user/following{/target}»,»gists_url»:»https://api.github.com/gists{/gist_id}»,»hub_url»:»https://api.github.com/hub»,»issue_search_url»:»https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}»,»issues_url»:»https://api.github.com/issues»,»keys_url»:»https://api.github.com/user/keys»,»notifications_url»:»https://api.github.com/notifications»,»organization_repositories_url»:»https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}»,»organization_url»:»https://api.github.com/orgs/{org}»,»public_gists_url»:»https://api.github.com/gists/public»,»rate_limit_url»:»https://api.github.com/rate_limit»,»repository_url»:»https://api.github.com/repos/{owner}/{repo}»,»repository_search_url»:»https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}»,»current_user_repositories_url»:»https://api.github.com/user/repos{?type,page,per_page,sort}»,»starred_url»:»https://api.github.com/user/starred{/owner}{/repo}»,»starred_gists_url»:»https://api.github.com/gists/starred»,»team_url»:»https://api.github.com/teams»,»user_url»:»https://api.github.com/users/{user}»,»user_organizations_url»:»https://api.github.com/user/orgs»,»user_repositories_url»:»https://api.github.com/users/{user}/repos{?type,page,per_page,sort}»,»user_search_url»:»https://api.github.com/search/users?q={query}{&page,per_page,sort,order}»}’ |
Использование .content
обеспечивает доступ к чистым байтам ответного пейлоада, то есть к любым данным в теле запроса. Однако, зачастую требуется конвертировать полученную информацию в строку в кодировке UTF-8. response
делает это при помощи .text
.
>>> response.text ‘{«current_user_url»:»https://api.github.com/user»,»current_user_authorizations_html_url»:»https://github.com/settings/connections/applications{/client_id}»,»authorizations_url»:»https://api.github.com/authorizations»,»code_search_url»:»https://api.github.com/search/code?q={query}{&page,per_page,sort,order}»,»commit_search_url»:»https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}»,»emails_url»:»https://api.github.com/user/emails»,»emojis_url»:»https://api.github.com/emojis»,»events_url»:»https://api.github.com/events»,»feeds_url»:»https://api.github.com/feeds»,»followers_url»:»https://api.github.com/user/followers»,»following_url»:»https://api.github.com/user/following{/target}»,»gists_url»:»https://api.github.com/gists{/gist_id}»,»hub_url»:»https://api.github.com/hub»,»issue_search_url»:»https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}»,»issues_url»:»https://api.github.com/issues»,»keys_url»:»https://api.github.com/user/keys»,»notifications_url»:»https://api.github.com/notifications»,»organization_repositories_url»:»https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}»,»organization_url»:»https://api.github.com/orgs/{org}»,»public_gists_url»:»https://api.github.com/gists/public»,»rate_limit_url»:»https://api.github.com/rate_limit»,»repository_url»:»https://api.github.com/repos/{owner}/{repo}»,»repository_search_url»:»https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}»,»current_user_repositories_url»:»https://api.github.com/user/repos{?type,page,per_page,sort}»,»starred_url»:»https://api.github.com/user/starred{/owner}{/repo}»,»starred_gists_url»:»https://api.github.com/gists/starred»,»team_url»:»https://api.github.com/teams»,»user_url»:»https://api.github.com/users/{user}»,»user_organizations_url»:»https://api.github.com/user/orgs»,»user_repositories_url»:»https://api.github.com/users/{user}/repos{?type,page,per_page,sort}»,»user_search_url»:»https://api.github.com/search/users?q={query}{&page,per_page,sort,order}»}’ |
Декодирование байтов в строку требует наличия определенной модели кодировки. По умолчанию requests
попытается узнать текущую кодировку, ориентируясь по заголовкам HTTP. Указать необходимую кодировку можно при помощи добавления .encoding
перед .text
.
>>> response.encoding = ‘utf-8’ # Optional: requests infers this internally >>> response.text ‘{«current_user_url»:»https://api.github.com/user»,»current_user_authorizations_html_url»:»https://github.com/settings/connections/applications{/client_id}»,»authorizations_url»:»https://api.github.com/authorizations»,»code_search_url»:»https://api.github.com/search/code?q={query}{&page,per_page,sort,order}»,»commit_search_url»:»https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}»,»emails_url»:»https://api.github.com/user/emails»,»emojis_url»:»https://api.github.com/emojis»,»events_url»:»https://api.github.com/events»,»feeds_url»:»https://api.github.com/feeds»,»followers_url»:»https://api.github.com/user/followers»,»following_url»:»https://api.github.com/user/following{/target}»,»gists_url»:»https://api.github.com/gists{/gist_id}»,»hub_url»:»https://api.github.com/hub»,»issue_search_url»:»https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}»,»issues_url»:»https://api.github.com/issues»,»keys_url»:»https://api.github.com/user/keys»,»notifications_url»:»https://api.github.com/notifications»,»organization_repositories_url»:»https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}»,»organization_url»:»https://api.github.com/orgs/{org}»,»public_gists_url»:»https://api.github.com/gists/public»,»rate_limit_url»:»https://api.github.com/rate_limit»,»repository_url»:»https://api.github.com/repos/{owner}/{repo}»,»repository_search_url»:»https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}»,»current_user_repositories_url»:»https://api.github.com/user/repos{?type,page,per_page,sort}»,»starred_url»:»https://api.github.com/user/starred{/owner}{/repo}»,»starred_gists_url»:»https://api.github.com/gists/starred»,»team_url»:»https://api.github.com/teams»,»user_url»:»https://api.github.com/users/{user}»,»user_organizations_url»:»https://api.github.com/user/orgs»,»user_repositories_url»:»https://api.github.com/users/{user}/repos{?type,page,per_page,sort}»,»user_search_url»:»https://api.github.com/search/users?q={query}{&page,per_page,sort,order}»}’ |
Если присмотреться к ответу, можно заметить, что его содержимое является сериализированным JSON контентом. Воспользовавшись словарем, можно взять полученные из .text
строки str
и провести с ними обратную сериализацию при помощи использования json.loads(). Есть и более простой способ, который требует применения .json()
.
>>> response.json() {‘current_user_url’: ‘https://api.github.com/user’, ‘current_user_authorizations_html_url’: ‘https://github.com/settings/connections/applications{/client_id}’, ‘authorizations_url’: ‘https://api.github.com/authorizations’, ‘code_search_url’: ‘https://api.github.com/search/code?q={query}{&page,per_page,sort,order}’, ‘commit_search_url’: ‘https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}’, ’emails_url’: ‘https://api.github.com/user/emails’, ’emojis_url’: ‘https://api.github.com/emojis’, ‘events_url’: ‘https://api.github.com/events’, ‘feeds_url’: ‘https://api.github.com/feeds’, ‘followers_url’: ‘https://api.github.com/user/followers’, ‘following_url’: ‘https://api.github.com/user/following{/target}’, ‘gists_url’: ‘https://api.github.com/gists{/gist_id}’, ‘hub_url’: ‘https://api.github.com/hub’, ‘issue_search_url’: ‘https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}’, ‘issues_url’: ‘https://api.github.com/issues’, ‘keys_url’: ‘https://api.github.com/user/keys’, ‘notifications_url’: ‘https://api.github.com/notifications’, ‘organization_repositories_url’: ‘https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}’, ‘organization_url’: ‘https://api.github.com/orgs/{org}’, ‘public_gists_url’: ‘https://api.github.com/gists/public’, ‘rate_limit_url’: ‘https://api.github.com/rate_limit’, ‘repository_url’: ‘https://api.github.com/repos/{owner}/{repo}’, ‘repository_search_url’: ‘https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}’, ‘current_user_repositories_url’: ‘https://api.github.com/user/repos{?type,page,per_page,sort}’, ‘starred_url’: ‘https://api.github.com/user/starred{/owner}{/repo}’, ‘starred_gists_url’: ‘https://api.github.com/gists/starred’, ‘team_url’: ‘https://api.github.com/teams’, ‘user_url’: ‘https://api.github.com/users/{user}’, ‘user_organizations_url’: ‘https://api.github.com/user/orgs’, ‘user_repositories_url’: ‘https://api.github.com/users/{user}/repos{?type,page,per_page,sort}’, ‘user_search_url’: ‘https://api.github.com/search/users?q={query}{&page,per_page,sort,order}’} |
Тип полученного значения из .json()
, является словарем. Это значит, что доступ к его содержимому можно получить по ключу.
Коды состояния и тело сообщения предоставляют огромный диапазон возможностей. Однако, для их оптимального использования требуется изучить метаданные и заголовки HTTP.
HTTP заголовки ответов на запрос могут предоставить определенную полезную информацию. Это может быть тип содержимого ответного пейлоада, а также ограничение по времени для кеширования ответа. Для просмотра HTTP заголовков загляните в атрибут .headers
.
>>> response.headers {‘Server’: ‘GitHub.com’, ‘Date’: ‘Mon, 10 Dec 2018 17:49:54 GMT’, ‘Content-Type’: ‘application/json; charset=utf-8’, ‘Transfer-Encoding’: ‘chunked’, ‘Status’: ‘200 OK’, ‘X-RateLimit-Limit’: ’60’, ‘X-RateLimit-Remaining’: ’59’, ‘X-RateLimit-Reset’: ‘1544467794’, ‘Cache-Control’: ‘public, max-age=60, s-maxage=60’, ‘Vary’: ‘Accept’, ‘ETag’: ‘W/»7dc470913f1fe9bb6c7355b50a0737bc»‘, ‘X-GitHub-Media-Type’: ‘github.v3; format=json’, ‘Access-Control-Expose-Headers’: ‘ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type’, ‘Access-Control-Allow-Origin’: ‘*’, ‘Strict-Transport-Security’: ‘max-age=31536000; includeSubdomains; preload’, ‘X-Frame-Options’: ‘deny’, ‘X-Content-Type-Options’: ‘nosniff’, ‘X-XSS-Protection’: ‘1; mode=block’, ‘Referrer-Policy’: ‘origin-when-cross-origin, strict-origin-when-cross-origin’, ‘Content-Security-Policy’: «default-src ‘none'», ‘Content-Encoding’: ‘gzip’, ‘X-GitHub-Request-Id’: ‘E439:4581:CF2351:1CA3E06:5C0EA741’} |
.headers
возвращает словарь, что позволяет получить доступ к значению заголовка HTTP по ключу. Например, для просмотра типа содержимого ответного пейлоада, требуется использовать Content-Type
.
>>> response.headers[‘Content-Type’] ‘application/json; charset=utf-8’ |
У объектов словарей в качестве заголовков есть своим особенности. Специфика HTTP предполагает, что заголовки не чувствительны к регистру. Это значит, что при получении доступа к заголовкам можно не беспокоится о том, использованы строчным или прописные буквы.
>>> response.headers[‘content-type’] ‘application/json; charset=utf-8’ |
При использовании ключей 'content-type'
и 'Content-Type'
результат будет получен один и тот же.
Это была основная информация, требуемая для работы с Response
. Были задействованы главные атрибуты и методы, а также представлены примеры их использования. В дальнейшем будет показано, как изменится ответ после настройки запроса GET
.
Python Requests параметры запроса
Наиболее простым способом настроить запрос GET
является передача значений через параметры строки запроса в URL. При использовании метода get()
, данные передаются в params
. Например, для того, чтобы посмотреть на библиотеку requests
можно использовать Search API на GitHub.
import requests # Поиск местонахождения для запросов на GitHub response = requests.get( ‘https://api.github.com/search/repositories’, params={‘q’: ‘requests+language:python’}, ) # Анализ некоторых атрибутов местонахождения запросов json_response = response.json() repository = json_response[‘items’][0] print(f‘Repository name: {repository[«name»]}’) # Python 3.6+ print(f‘Repository description: {repository[«description»]}’) # Python 3.6+ |
Передавая словарь {'q': 'requests+language:python'}
в параметр params
, который является частью .get()
, можно изменить ответ, что был получен при использовании Search API
.
Можно передать параметры в get()
в форме словаря, как было показано выше. Также можно использовать список кортежей.
>>> requests.get( ... ‘https://api.github.com/search/repositories’, ... params=[(‘q’, ‘requests+language:python’)], ... ) <Response [200]> |
Также можно передать значение в байтах.
>>> requests.get( ... ‘https://api.github.com/search/repositories’, ... params=b‘q=requests+language:python’, ... ) <Response [200]> |
Строки запроса полезны для уточнения параметров в запросах GET
. Также можно настроить запросы при помощи добавления или изменения заголовков отправленных сообщений.
Для изменения HTTP заголовка требуется передать словарь данного HTTP заголовка в get()
при помощи использования параметра headers
. Например, можно изменить предыдущий поисковой запрос, подсветив совпадения в результате. Для этого в заголовке Accept
медиа тип уточняется при помощи text-match
.
import requests response = requests.get( ‘https://api.github.com/search/repositories’, params={‘q’: ‘requests+language:python’}, headers={‘Accept’: ‘application/vnd.github.v3.text-match+json’}, ) # просмотр нового массива `text-matches` с предоставленными данными # о поиске в пределах результатов json_response = response.json() repository = json_response[‘items’][0] print(f‘Text matches: {repository[«text_matches»]}’) |
Заголовок Accept
сообщает серверу о типах контента, который можно использовать в рассматриваемом приложении. Здесь подразумевается, что все совпадения будут подсвечены, для чего в заголовке используется значение application/vnd.github.v3.text-match+json
. Это уникальный заголовок Accept
для GitHub. В данном случае содержимое представлено в специальном JSON формате.
Перед более глубоким изучением способов редактирования запросов, будет не лишним остановиться на некоторых других методах HTTP.
Примеры HTTP методов в Requests
Помимо GET
, большой популярностью пользуются такие методы, как POST
, PUT
, DELETE
, HEAD
, PATCH
и OPTIONS
. Для каждого из этих методов существует своя сигнатура, которая очень похожа на метод get()
.
>>> requests.post(‘https://httpbin.org/post’, data={‘key’:‘value’}) >>> requests.put(‘https://httpbin.org/put’, data={‘key’:‘value’}) >>> requests.delete(‘https://httpbin.org/delete’) >>> requests.head(‘https://httpbin.org/get’) >>> requests.patch(‘https://httpbin.org/patch’, data={‘key’:‘value’}) >>> requests.options(‘https://httpbin.org/get’) |
Каждая функция создает запрос к httpbin
сервису, используя при этом ответный HTTP метод. Результат каждого метода можно изучить способом, который был использован в предыдущих примерах.
>>> response = requests.head(‘https://httpbin.org/get’) >>> response.headers[‘Content-Type’] ‘application/json’ >>> response = requests.delete(‘https://httpbin.org/delete’) >>> json_response = response.json() >>> json_response[‘args’] {} |
При использовании каждого из данных методов в Response
могут быть возвращены заголовки, тело запроса, коды состояния и многие другие аспекты. Методы POST
, PUT
и PATCH
в дальнейшем будут описаны более подробно.
Python Requests тело сообщения
В соответствии со спецификацией HTTP запросы POST
, PUT
и PATCH
передают информацию через тело сообщения, а не через параметры строки запроса. Используя requests
, можно передать данные в параметр data
.
В свою очередь data
использует словарь, список кортежей, байтов или объект файла. Это особенно важно, так как может возникнуть необходимость адаптации отправляемых с запросом данных в соответствии с определенными параметрами сервера.
К примеру, если тип содержимого запроса application/x-www-form-urlencoded
, можно отправить данные формы в виде словаря.
>>> requests.post(‘https://httpbin.org/post’, data={‘key’:‘value’}) <Response [200]> |
Ту же самую информацию также можно отправить в виде списка кортежей.
>>> requests.post(‘https://httpbin.org/post’, data=[(‘key’, ‘value’)]) <Response [200]> |
В том случае, если требуется отравить данные JSON, можно использовать параметр json
. При передачи данных JSON через json
, requests
произведет сериализацию данных и добавит правильный Content-Type
заголовок.
Стоит взять на заметку сайт httpbin.org. Это чрезвычайно полезный ресурс, созданный человеком, который внедрил использование requests
– Кеннетом Рейтцом. Данный сервис предназначен для тестовых запросов. Здесь можно составить пробный запрос и получить ответ с требуемой информацией. В качестве примера рассмотрим базовый запрос с использованием POST
.
>>> response = requests.post(‘https://httpbin.org/post’, json={‘key’:‘value’}) >>> json_response = response.json() >>> json_response[‘data’] ‘{«key»: «value»}’ >>> json_response[‘headers’][‘Content-Type’] ‘application/json’ |
Здесь видно, что сервер получил данные и HTTP заголовки, отправленные вместе с запросом. requests
также предоставляет информацию в форме PreparedRequest
.
Python Requests анализ запроса
При составлении запроса стоит иметь в виду, что перед его фактической отправкой на целевой сервер библиотека requests
выполняет определенную подготовку. Подготовка запроса включает в себя такие вещи, как проверка заголовков и сериализация содержимого JSON.
Если открыть .request
, можно просмотреть PreparedRequest
.
>>> response = requests.post(‘https://httpbin.org/post’, json={‘key’:‘value’}) >>> response.request.headers[‘Content-Type’] ‘application/json’ >>> response.request.url ‘https://httpbin.org/post’ >>> response.request.body b‘{«key»: «value»}’ |
Проверка PreparedRequest
открывает доступ ко всей информации о выполняемом запросе. Это может быть пейлоад, URL, заголовки, аутентификация и многое другое.
У всех описанных ранее типов запросов была одна общая черта – они представляли собой неаутентифицированные запросы к публичным API. Однако, подобающее большинство служб, с которыми может столкнуться пользователь, запрашивают аутентификацию.
Python Requests аутентификация HTTP AUTH
Аутентификация помогает сервису понять, кто вы. Как правило, вы предоставляете свои учетные данные на сервер, передавая данные через заголовок Authorization
или пользовательский заголовок, определенной службы. Все функции запроса, которые вы видели до этого момента, предоставляют параметр с именем auth
, который позволяет вам передавать свои учетные данные.
Одним из примеров API, который требует аутентификации, является Authenticated User API на GitHub. Это конечная точка веб-сервиса, которая предоставляет информацию о профиле аутентифицированного пользователя. Чтобы отправить запрос API-интерфейсу аутентифицированного пользователя, вы можете передать свое имя пользователя и пароль на GitHub через кортеж в get()
.
>>> from getpass import getpass >>> requests.get(‘https://api.github.com/user’, auth=(‘username’, getpass())) <Response [200]> |
Запрос выполнен успешно, если учетные данные, которые вы передали в кортеже auth
, действительны. Если вы попытаетесь сделать этот запрос без учетных данных, вы увидите, что код состояния 401 Unauthorized
.
>>> requests.get(‘https://api.github.com/user’) <Response [401]> |
Когда вы передаете имя пользователя и пароль в кортеже параметру auth
, вы используете учетные данные при помощи базовой схемы аутентификации HTTP.
Таким образом, вы можете создать тот же запрос, передав подробные учетные данные базовой аутентификации, используя HTTPBasicAuth
.
>>> from requests.auth import HTTPBasicAuth >>> from getpass import getpass >>> requests.get( ... ‘https://api.github.com/user’, ... auth=HTTPBasicAuth(‘username’, getpass()) ... ) <Response [200]> |
Хотя вам не нужно явно указывать обычную аутентификацию, может потребоваться аутентификация с использованием другого метода. requests
предоставляет другие методы аутентификации, например, HTTPDigestAuth
и HTTPProxyAuth
.
Вы даже можете предоставить свой собственный механизм аутентификации. Для этого необходимо сначала создать подкласс AuthBase. Затем происходит имплементация __call__()
.
import requests from requests.auth import AuthBase class TokenAuth(AuthBase): «»»Implements a custom authentication scheme.»»» def __init__(self, token): self.token = token def __call__(self, r): «»»Attach an API token to a custom auth header.»»» r.headers[‘X-TokenAuth’] = f‘{self.token}’ # Python 3.6+ return r requests.get(‘https://httpbin.org/get’, auth=TokenAuth(‘12345abcde-token’)) |
Здесь пользовательский механизм TokenAuth
получает специальный токен. Затем этот токен включается заголовок X-TokenAuth
запроса.
Плохие механизмы аутентификации могут привести к уязвимостям безопасности, поэтому, если службе по какой-то причине не нужен настраиваемый механизм аутентификации, вы всегда можете использовать проверенную схему аутентификации, такую как Basic или OAuth.
Пока вы думаете о безопасности, давайте рассмотрим использование requests
в SSL сертификатах.
Python Requests проверка SSL сертификата
Всякий раз, когда данные, которые вы пытаетесь отправить или получить, являются конфиденциальными, безопасность важна. Вы общаетесь с защищенными сайтами через HTTP, устанавливая зашифрованное соединение с использованием SSL, что означает, что проверка SSL сертификата целевого сервера имеет решающее значение.
Хорошей новостью является то, что requests
по умолчанию все делает сам. Однако в некоторых случаях необходимо внести определенные поправки.
Если требуется отключить проверку SSL-сертификата, параметру verify
функции запроса можно присвоить значение False
.
>>> requests.get(‘https://api.github.com’, verify=False) InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced—usage.html#ssl-warnings InsecureRequestWarning) <Response [200]> |
В случае небезопасного запроса requests
предупреждает о возможности потери информации и просит сохранить данные или отказаться от запроса.
Примечание. Для предоставления сертификатов
requests
использует пакет, который вызываетсяcertifi
. Это дает понятьrequests
, каким ответам можно доверять. Поэтому вам следует часто обновлятьcertifi
, чтобы обеспечить максимальную безопасность ваших соединений.
Python Requests производительность приложений
При использовании requests
, особенно в среде приложений, важно учитывать влияние на производительность. Такие функции, как контроль таймаута, сеансы и ограничения повторных попыток, могут помочь обеспечить бесперебойную работу приложения.
Таймауты
Когда вы отправляете встроенный запрос во внешнюю службу, вашей системе нужно будет дождаться ответа, прежде чем двигаться дальше. Если ваше приложение слишком долго ожидает ответа, запросы к службе могут быть сохранены, пользовательский интерфейс может пострадать или фоновые задания могут зависнуть.
По умолчанию в requests на ответ время не ограничено, и весь процесс может занять значительный промежуток. По этой причине вы всегда должны указывать время ожидания, чтобы такого не происходило. Чтобы установить время ожидания запроса, используйте параметр timeout
. timeout
может быть целым числом или числом с плавающей точкой, представляющим количество секунд ожидания ответа до истечения времени ожидания.
>>> requests.get(‘https://api.github.com’, timeout=1) <Response [200]> >>> requests.get(‘https://api.github.com’, timeout=3.05) <Response [200]> |
В первом примере запрос истекает через 1 секунду. Во втором примере запрос истекает через 3,05 секунды.
Вы также можете передать кортеж. Это – таймаут соединения (время, за которое клиент может установить соединение с сервером), а второй – таймаут чтения (время ожидания ответа, как только ваш клиент установил соединение):
>>> requests.get(‘https://api.github.com’, timeout=(2, 5)) <Response [200]> |
Если запрос устанавливает соединение в течение 2 секунд и получает данные в течение 5 секунд после установления соединения, то ответ будет возвращен, как это было раньше. Если время ожидания истекло, функция вызовет исключение Timeout
.
import requests from requests.exceptions import Timeout try: response = requests.get(‘https://api.github.com’, timeout=1) except Timeout: print(‘The request timed out’) else: print(‘The request did not time out’) |
Ваша программа может поймать исключение Timeout
и ответить соответственно.
Объект Session в Requests
До сих пор вы имели дело с requests
API высокого уровня, такими как get()
и post()
. Эти функции являются абстракцией того, что происходит, когда вы делаете свои запросы. Они скрывают детали реализации, такие как управление соединениями, так что вам не нужно о них беспокоиться.
Под этими абстракциями находится класс под названием Session
. Если вам необходимо настроить контроль над выполнением запросов или повысить производительность ваших запросов, вам может потребоваться использовать Session
напрямую.
Сессии используются для сохранения параметров в запросах.
Например, если вы хотите использовать одну и ту же аутентификацию для нескольких запросов, вы можете использовать сеанс:
import requests from getpass import getpass # используя менеджер контента, можно убедиться, что ресурсы, применимые # во время сессии будут свободны после использования with requests.Session() as session: session.auth = (‘username’, getpass()) # Instead of requests.get(), you’ll use session.get() response = session.get(‘https://api.github.com/user’) # здесь можно изучить ответ print(response.headers) print(response.json()) |
Каждый раз, когда вы делаете запрос session
, после того как он был инициализирован с учетными данными аутентификации, учетные данные будут сохраняться.
Первичная оптимизация производительности сеансов происходит в форме постоянных соединений. Когда ваше приложение устанавливает соединение с сервером с помощью Session
, оно сохраняет это соединение в пуле соединений. Когда ваше приложение снова хочет подключиться к тому же серверу, оно будет использовать соединение из пула, а не устанавливать новое.
HTTPAdapter — Максимальное количество повторов запроса в Requests
В случае сбоя запроса возникает необходимость сделать повторный запрос. Однако requests
не будет делать это самостоятельно. Для применения функции повторного запроса требуется реализовать собственный транспортный адаптер.
Транспортные адаптеры позволяют определить набор конфигураций для каждой службы, с которой вы взаимодействуете. Предположим, вы хотите, чтобы все запросы к https://api.github.com
были повторены три раза, прежде чем, наконец, появится ConnectionError
. Для этого нужно построить транспортный адаптер, установить его параметр max_retries
и подключить его к существующему объекту Session
.
import requests from requests.adapters import HTTPAdapter from requests.exceptions import ConnectionError github_adapter = HTTPAdapter(max_retries=3) session = requests.Session() # использование `github_adapter` для всех запросов, которые начинаются с указанным URL session.mount(‘https://api.github.com’, github_adapter) try: session.get(‘https://api.github.com’) except ConnectionError as ce: print(ce) |
При установке HTTPAdapter
, github_adapter
к session
, session
будет придерживаться своей конфигурации для каждого запроса к https://api.github.com
.
Таймауты, транспортные адаптеры и сессии предназначены для обеспечения эффективности используемого кода и стабильности приложения.
Заключение
Изучение библиотеки Python requests является очень трудоемким процессом.
После разбора данных в статье примеров можно научиться тому, как:
- Создавать запросы, используя различные методы HTTP –
GET
,POST
иPUT
; - Настраивать свои запросы, изменив заголовки, аутентификацию, строки запросов и тела сообщений;
- Проверять данные, которые были отправлены на сервер, а также те данные, которые сервер отправил обратно;
- Работать с проверкой SSL сертификата;
- Эффективно использовать
requests
,max_retries
,timeout
,Sessions
и транспортные адаптеры.
Грамотное использование requests позволит наиболее эффективно настроить разрабатываемые приложения, исследуя широкий спектр веб-сервисов и данных, опубликованных на них.
- Данная статья является переводом статьи: Python’s Requests Library (Guide)
- Изображение статьи принадлежит сайту © RealPython
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»