fetch
|
@ -8,9 +8,9 @@ That's what lookahead is for.
|
|||
|
||||
## Lookahead
|
||||
|
||||
The syntax is: `pattern:x(?=y)`, it means "match `pattern:x` only if followed by `pattern:y`".
|
||||
The syntax is: `pattern:x(?=y)`, it means "look for `pattern:x`, but match only if followed by `pattern:y`".
|
||||
|
||||
The euro sign is often written after the amount, so the regexp will be `pattern:\d+(?=€)` (assuming the price has no decimal point):
|
||||
For an integer amount followed by `subject:€`, the regexp will be `pattern:\d+(?=€)`:
|
||||
|
||||
```js run
|
||||
let str = "1 turkey costs 30€";
|
||||
|
@ -18,9 +18,11 @@ let str = "1 turkey costs 30€";
|
|||
alert( str.match(/\d+(?=€)/) ); // 30 (correctly skipped the sole number 1)
|
||||
```
|
||||
|
||||
Or, if we wanted a quantity, then a negative lookahead can be applied.
|
||||
Let's say we want a quantity instead, that is a number, NOT followed by `subject:€`.
|
||||
|
||||
The syntax is: `pattern:x(?!y)`, it means "match `pattern:x` only if not followed by `pattern:y`".
|
||||
Here a negative lookahead can be applied.
|
||||
|
||||
The syntax is: `pattern:x(?!y)`, it means "search `pattern:x`, but only if not followed by `pattern:y`".
|
||||
|
||||
```js run
|
||||
let str = "2 turkeys cost 60€";
|
||||
|
@ -30,39 +32,43 @@ alert( str.match(/\d+(?!€)/) ); // 2 (correctly skipped the price)
|
|||
|
||||
## Lookbehind
|
||||
|
||||
Lookbehind allows to match a pattern only if there's something before.
|
||||
Lookahead allows to add a condition for "what goes after".
|
||||
|
||||
Lookbehind is similar, but it looks behind. That is, it allows to match a pattern only if there's something before.
|
||||
|
||||
The syntax is:
|
||||
- Positive lookbehind: `pattern:(?<=y)x`, matches `pattern:x`, but only if it follows after `pattern:y`.
|
||||
- Negative lookbehind: `pattern:(?<!y)x`, matches `pattern:x`, but only if there's no `pattern:y` before.
|
||||
|
||||
For example, let's change the price to US dollars. The dollar sign is usually before the number, so to look for `$30` we'll use `pattern:(?<=\$)\d+`:
|
||||
For example, let's change the price to US dollars. The dollar sign is usually before the number, so to look for `$30` we'll use `pattern:(?<=\$)\d+` -- an amount preceeded by `subject:$`:
|
||||
|
||||
```js run
|
||||
let str = "1 turkey costs $30";
|
||||
|
||||
alert( str.match(/(?<=\$)\d+/) ); // 30 (correctly skipped the sole number 1)
|
||||
alert( str.match(/(?<=\$)\d+/) ); // 30 (skipped the sole number)
|
||||
```
|
||||
|
||||
And for the quantity let's use a negative lookbehind `pattern:(?<!\$)\d+`:
|
||||
And, to find the quantity -- a number, not preceeded by `subject:$`, we can use a negative lookbehind `pattern:(?<!\$)\d+`:
|
||||
|
||||
```js run
|
||||
let str = "2 turkeys cost $60";
|
||||
|
||||
alert( str.match(/(?<!\$)\d+/) ); // 2 (correctly skipped the price)
|
||||
alert( str.match(/(?<!\$)\d+/) ); // 2 (skipped the price)
|
||||
```
|
||||
|
||||
## Capture groups
|
||||
|
||||
Generally, what's inside the lookaround (a common name for both lookahead and lookbehind) parentheses does not become a part of the match.
|
||||
|
||||
But if we want to capture something, that's doable. Just need to wrap that into additional parentheses.
|
||||
E.g. in the pattern `pattern:\d+(?!€)`, the `pattern:€` sign doesn't get captured as a part of the match.
|
||||
|
||||
But if we want to capture the whole lookaround expression or a part of it, that's possible. Just need to wrap that into additional parentheses.
|
||||
|
||||
For instance, here the currency `pattern:(€|kr)` is captured, along with the amount:
|
||||
|
||||
```js run
|
||||
let str = "1 turkey costs 30€";
|
||||
let reg = /\d+(?=(€|kr))/;
|
||||
let reg = /\d+(?=(€|kr))/; // extra parentheses around €|kr
|
||||
|
||||
alert( str.match(reg) ); // 30, €
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
# Fetch
|
||||
# Fetch basics
|
||||
|
||||
Method `fetch()` is the modern way of sending requests over HTTP.
|
||||
|
||||
|
@ -104,7 +104,7 @@ img.style = 'position:fixed;top:10px;left:10px;width:100px';
|
|||
setTimeout(() => img.remove(), 2000);
|
||||
```
|
||||
|
||||
## The full syntax
|
||||
## Fetch API in detail
|
||||
|
||||
The second argument provides a lot of flexibility to `fetch` syntax.
|
||||
|
||||
|
@ -116,17 +116,21 @@ let promise = fetch(url, {
|
|||
headers: {
|
||||
"Content-Type": "text/plain;charset=UTF-8"
|
||||
},
|
||||
destination: "", // audio, audioworklet, document, embed, font...
|
||||
body: undefined // string, FormData, Blob, BufferSource, or URLSearchParams
|
||||
referrer: "about:client", // "" for no-referrer, or an url from the current origin
|
||||
referrerPolicy: "", // no-referrer, no-referrer-when-downgrade, same-origin...
|
||||
mode: "cors", // same-origin, no-cors, navigate, or websocket
|
||||
credentials: "same-origin", // omit, include
|
||||
cache: "default", // no-store, reload, no-cache, force-cache, or only-if-cached
|
||||
redirect: "follow", // manual, error
|
||||
integrity: "" // a hash, like "sha256-abcdef1234567890"
|
||||
keepalive: false // true
|
||||
body: "string" // FormData, Blob, BufferSource, or URLSearchParams
|
||||
integrity: "", // a hash, like "sha256-abcdef1234567890"
|
||||
keepalive: false, // true
|
||||
signal: undefined, // AbortController to abort request
|
||||
window: window // null
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 290 B |
|
@ -0,0 +1,9 @@
|
|||
`Origin` нужен, потому что `Referer` передаётся не всегда. В частности, при запросе с HTTPS на HTTP -- нет `Referer`.
|
||||
|
||||
Политика [Content Security Policy](http://en.wikipedia.org/wiki/Content_Security_Policy) может запрещать пересылку `Referer`.
|
||||
|
||||
По стандарту `Referer` является необязательным HTTP-заголовком, в некоторых браузерах есть настройки, которые запрещают его слать.
|
||||
|
||||
Именно поэтому, ввиду того, что на `Referer` полагаться нельзя, и придумали заголовок `Origin`, который гарантированно присылается при кросс-доменных запросах.
|
||||
|
||||
Что же касается "неправильного" `Referer` -- это из области фантастики. Когда-то, много лет назад, в браузерах были ошибки, которые позволяли подменить `Referer` из JavaScript, но они давно исправлены. Никакая "злая страница" не может его подменить.
|
30
7-network/2-fetch-crossorigin/1-do-we-need-origin/task.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Зачем нужен Origin?
|
||||
|
||||
Как вы, наверняка, знаете, существует HTTP-заголовок `Referer`, в котором обычно указан адрес страницы, с которой инициирован запрос.
|
||||
|
||||
Например, при отправке `XMLHttpRequest` со страницы `http://javascript.ru/some/url` на `http://google.ru`, заголовки будут примерно такими:
|
||||
|
||||
```
|
||||
Accept:*/*
|
||||
Accept-Charset:windows-1251,utf-8;q=0.7,*;q=0.3
|
||||
Accept-Encoding:gzip,deflate,sdch
|
||||
Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4
|
||||
Connection:keep-alive
|
||||
Host:google.ru
|
||||
*!*
|
||||
Origin:http://javascript.ru
|
||||
Referer:http://javascript.ru/some/url
|
||||
*/!*
|
||||
```
|
||||
|
||||
Как видно, здесь присутствуют и `Referer` и `Origin`.
|
||||
|
||||
Итак, вопросы:
|
||||
|
||||
1. Зачем нужен `Origin`, если `Referer` содержит даже более полную информацию?
|
||||
2. Может ли быть такое, что заголовка `Referer` нет или он неправильный?
|
||||
|
378
7-network/2-fetch-crossorigin/article.md
Normal file
|
@ -0,0 +1,378 @@
|
|||
# Cross-Origin Fetch
|
||||
|
||||
If we make a `fetch` from an arbitrary web-site, that will probably fail.
|
||||
|
||||
Fetching from another origin (domain/port/protocol triplet) requires special headers from the remote side.
|
||||
|
||||
For instance, let's try fetching from http://google.com:
|
||||
|
||||
```js run async
|
||||
try {
|
||||
await fetch('http://google.com');
|
||||
} catch(err) {
|
||||
alert(err); // Failed to fetch
|
||||
}
|
||||
```
|
||||
|
||||
Fetch fails, as expected.
|
||||
|
||||
## Safety control
|
||||
|
||||
Cross-origin requests pass a special safety control, with the sole purpose to protect the internet from evil hackers.
|
||||
|
||||
Seriously. For many years cross-domain requests were simply unavailable. The internet got used to it, people got used to it.
|
||||
|
||||
Imagine for a second that a new standard appeared, that allows any webpage to make any requests anywhere.
|
||||
|
||||
How an evil hacker could use it?
|
||||
|
||||
They would create a page at `http://evilhacker.com`, lure a user to it, and run `fetch` from his mail server, e.g. `http://gmail.com`.
|
||||
|
||||
|
||||
|
||||
When cross-origin requests were finally implemented, safety restrictions were placed to prevent an evil-minded person from doing anything that they couldn't do before. In such a way, that old sites are automatically protected.
|
||||
|
||||
No script could fetch a webpage from another site (another origin, to be precise).
|
||||
|
||||
That becomes really important, as you remember about cookies. Any request brings cookies by default, so being able to
|
||||
|
||||
can't access the content of a page from another site.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Кросс-доменные запросы проходят специальный контроль безопасности, цель которого -- не дать злым хакерам(tm) завоевать интернет.
|
||||
|
||||
Серьёзно. Разработчики стандарта предусмотрели все заслоны, чтобы "злой хакер" не смог, воспользовавшись новым стандартом, сделать что-то принципиально отличное от того, что и так мог раньше и, таким образом, "сломать" какой-нибудь сервер, работающий по-старому стандарту и не ожидающий ничего принципиально нового.
|
||||
|
||||
Давайте, на минуточку, вообразим, что появился стандарт, который даёт, без ограничений, возможность делать любой странице HTTP-запросы куда угодно, какие угодно.
|
||||
|
||||
Как сможет этим воспользоваться злой хакер?
|
||||
|
||||
Он сделает свой сайт, например `http://evilhacker.com` и заманит туда посетителя (а может посетитель попадёт на "злонамеренную" страницу и по ошибке -- не так важно).
|
||||
|
||||
Когда посетитель зайдёт на `http://evilhacker.com`, он автоматически запустит JS-скрипт на странице. Этот скрипт сделает HTTP-запрос на почтовый сервер, к примеру, `http://gmail.com`. А ведь обычно HTTP-запросы идут с куками посетителя и другими авторизующими заголовками.
|
||||
|
||||
Поэтому хакер сможет написать на `http://evilhacker.com` код, который, сделав GET-запрос на `http://gmail.com`, получит информацию из почтового ящика посетителя. Проанализирует её, сделает ещё пачку POST-запросов для отправки писем от имени посетителя. Затем настанет очередь онлайн-банка и так далее.
|
||||
|
||||
Спецификация [CORS](http://www.w3.org/TR/cors/) налагает специальные ограничения на запросы, которые призваны не допустить подобного апокалипсиса.
|
||||
|
||||
Запросы в ней делятся на два вида.
|
||||
|
||||
[Простыми](http://www.w3.org/TR/cors/#terminology) считаются запросы, если они удовлетворяют следующим двум условиям:
|
||||
|
||||
1. [Простой метод](http://www.w3.org/TR/cors/#simple-method): GET, POST или HEAD
|
||||
2. [Простые заголовки](http://www.w3.org/TR/cors/#simple-header) -- только из списка:
|
||||
|
||||
- `Accept`
|
||||
- `Accept-Language`
|
||||
- `Content-Language`
|
||||
- `Content-Type` со значением `application/x-www-form-urlencoded`, `multipart/form-data` или `text/plain`.
|
||||
|
||||
"Непростыми" считаются все остальные, например, запрос с методом `PUT` или с заголовком `Authorization` не подходит под ограничения выше.
|
||||
|
||||
Принципиальная разница между ними заключается в том, что "простой" запрос можно сформировать и отправить на сервер и без XMLHttpRequest, например при помощи HTML-формы.
|
||||
|
||||
То есть, злой хакер на странице `http://evilhacker.com` и до появления CORS мог отправить произвольный GET-запрос куда угодно. Например, если создать и добавить в документ элемент `<script src="любой url">`, то браузер сделает GET-запрос на этот URL.
|
||||
|
||||
Аналогично, злой хакер и ранее мог на своей странице объявить и, при помощи JavaScript, отправить HTML-форму с методом GET/POST и кодировкой `multipart/form-data`. А значит, даже старый сервер наверняка предусматривает возможность таких атак и умеет от них защищаться.
|
||||
|
||||
А вот запросы с нестандартными заголовками или с методом `DELETE` таким образом не создать. Поэтому старый сервер может быть к ним не готов. Или, к примеру, он может полагать, что такие запросы веб-страница в принципе не умеет присылать, значит они пришли из привилегированного приложения, и дать им слишком много прав.
|
||||
|
||||
Поэтому при посылке "непростых" запросов нужно специальным образом спросить у сервера, согласен ли он в принципе на подобные кросс-доменные запросы или нет? И, если сервер не ответит, что согласен -- значит, нет.
|
||||
|
||||
```summary
|
||||
В спецификации CORS, как мы увидим далее, есть много деталей, но все они объединены единым принципом: новые возможности доступны только с явного согласия сервера (по умолчанию -- нет).
|
||||
```
|
||||
|
||||
## CORS для простых запросов
|
||||
|
||||
В кросс-доменный запрос браузер автоматически добавляет заголовок `Origin`, содержащий домен, с которого осуществлён запрос.
|
||||
|
||||
В случае запроса на `http://anywhere.com/request` с `http://javascript.ru/page` заголовки будут примерно такие:
|
||||
|
||||
```
|
||||
GET /request
|
||||
Host:anywhere.com
|
||||
*!*
|
||||
Origin:http://javascript.ru
|
||||
*/!*
|
||||
...
|
||||
```
|
||||
|
||||
Сервер должен, со своей стороны, ответить специальными заголовками, разрешает ли он такой запрос к себе.
|
||||
|
||||
Если сервер разрешает кросс-доменный запрос с этого домена -- он должен добавить к ответу заголовок `Access-Control-Allow-Origin`, содержащий домен запроса (в данном случае "javascript.ru") или звёздочку `*`.
|
||||
|
||||
**Только при наличии такого заголовка в ответе -- браузер сочтёт запрос успешным, а иначе JavaScript получит ошибку.**
|
||||
|
||||

|
||||
|
||||
То есть, ответ сервера может быть примерно таким:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type:text/html; charset=UTF-8
|
||||
*!*
|
||||
Access-Control-Allow-Origin: http://javascript.ru
|
||||
*/!*
|
||||
```
|
||||
|
||||
Если `Access-Control-Allow-Origin` нет, то браузер считает, что разрешение не получено, и завершает запрос с ошибкой.
|
||||
|
||||
При таких запросах не передаются куки и заголовки HTTP-авторизации. Параметры `user` и `password` в методе `open` игнорируются. Мы рассмотрим, как разрешить их передачу, чуть далее.
|
||||
|
||||
```warn header="Что может сделать хакер, используя такие запросы?"
|
||||
Описанные выше ограничения приводят к тому, что запрос полностью безопасен.
|
||||
|
||||
Действительно, злая страница может сформировать любой GET/POST-запрос и отправить его, но без разрешения сервера ответа она не получит.
|
||||
|
||||
А без ответа такой запрос, по сути, эквивалентен отправке формы GET/POST, причём без авторизации.
|
||||
```
|
||||
|
||||
## Ограничения IE9-
|
||||
|
||||
В IE9- используется `XDomainRequest`, который представляет собой урезанный `XMLHttpRequest`.
|
||||
|
||||
На него действуют ограничения:
|
||||
|
||||
- Протокол нужно сохранять: запросы допустимы с HTTP на HTTP, с HTTPS на HTTPS. Другие протоколы запрещены.
|
||||
- Метод `open(method, url)` имеет только два параметра. Он всегда асинхронный.
|
||||
- Ряд возможностей современного стандарта недоступны, в частности:
|
||||
- Недоступны методы, кроме GET или POST.
|
||||
- Нельзя добавлять свои заголовки, даже нельзя указать свой `Content-Type` для запроса, он всегда `text/plain`.
|
||||
- Нельзя включить передачу кук и данных HTTP-авторизации.
|
||||
- В IE8 в режиме просмотра InPrivate кросс-доменные запросы не работают.
|
||||
|
||||
Современный стандарт [XMLHttpRequest](http://www.w3.org/TR/XMLHttpRequest/) предусматривает средства для преодоления этих ограничений, но на момент выхода IE8 они ещё не были проработаны, поэтому их не реализовали. А IE9 исправил некоторые ошибки, но в общем не добавил ничего нового.
|
||||
|
||||
Поэтому на сайтах, которые хотят поддерживать IE9-, то на практике кросс-доменные запросы редко используют, предпочитая другие способы кросс-доменной коммуникации. Например, динамически создаваемый тег `SCRIPT` или вспомогательный `IFRAME` с другого домена. Мы разберём эти подходы в последующих главах.
|
||||
|
||||
```smart header="Как разрешить кросс-доменные запросы от доверенного сайта в IE9-?"
|
||||
Разрешить кросс-доменные запросы для "доверенных" сайтов можно в настройках IE, во вкладке "Безопасность", включив пункт "Доступ к источникам данных за пределами домена".
|
||||
|
||||
Обычно это делается для зоны "Надёжные узлы", после чего в неё вносится доверенный сайт. Теперь он может делать кросс-доменные запросы `XMLHttpRequest`.
|
||||
|
||||
Этот способ можно применить для корпоративных сайтов, а также в тех случаях, когда посетитель заведомо вам доверяет, но почему-то (компьютер на работе, админ запрещает ставить другой браузер?) хочет использовать именно IE. Например, он может предлагаться в качестве дополнительной инструкции "как заставить этот сервис работать под IE".
|
||||
```
|
||||
|
||||
```smart header="В IE разрешён другой порт"
|
||||
В кросс-доменные ограничения IE не включён порт.
|
||||
|
||||
То есть, можно сделать запрос с `http://javascript.ru` на `http://javascript.ru:8080`, и в IE он не будет считаться кросс-доменным.
|
||||
|
||||
Это позволяет решить некоторые задачи, связанные с взаимодействием различных сервисов в рамках одного сайта. Но только для IE.
|
||||
```
|
||||
|
||||
Расширенные возможности, описанные далее, поддерживаются всеми современными браузерами, кроме IE9-.
|
||||
|
||||
## Заголовки ответа
|
||||
|
||||
Чтобы JavaScript мог прочитать HTTP-заголовок ответа, сервер должен указать его имя в `Access-Control-Expose-Headers`.
|
||||
|
||||
Например:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type:text/html; charset=UTF-8
|
||||
Access-Control-Allow-Origin: http://javascript.ru
|
||||
*!*
|
||||
X-Uid: 123
|
||||
X-Authorization: 2c9de507f2c54aa1
|
||||
Access-Control-Expose-Headers: X-Uid, X-Authentication
|
||||
*/!*
|
||||
```
|
||||
|
||||
По умолчанию скрипт может прочитать из ответа только "простые" заголовки:
|
||||
|
||||
```
|
||||
Cache-Control
|
||||
Content-Language
|
||||
Content-Type
|
||||
Expires
|
||||
Last-Modified
|
||||
Pragma
|
||||
```
|
||||
|
||||
...То есть, `Content-Type` получить всегда можно, а доступ к специфическим заголовкам нужно открывать явно.
|
||||
|
||||
## Запросы от имени пользователя
|
||||
|
||||
По умолчанию браузер не передаёт с запросом куки и авторизующие заголовки.
|
||||
|
||||
Чтобы браузер передал вместе с запросом куки и HTTP-авторизацию, нужно поставить запросу `xhr.withCredentials = true`:
|
||||
|
||||
```js
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.withCredentials = true;
|
||||
|
||||
xhr.open('POST', 'http://anywhere.com/request', true)
|
||||
...
|
||||
```
|
||||
|
||||
Далее -- всё как обычно, дополнительных действий со стороны клиента не требуется.
|
||||
|
||||
Такой `XMLHttpRequest` с куками, естественно, требует от сервера больше разрешений, чем "анонимный".
|
||||
|
||||
**Поэтому для запросов с `withCredentials` предусмотрено дополнительное подтверждение со стороны сервера.**
|
||||
|
||||
При запросе с `withCredentials` сервер должен вернуть уже не один, а два заголовка:
|
||||
|
||||
- `Access-Control-Allow-Origin: домен`
|
||||
- `Access-Control-Allow-Credentials: true`
|
||||
|
||||
Пример заголовков:
|
||||
|
||||
```js no-beautify
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type:text/html; charset=UTF-8
|
||||
*!*
|
||||
Access-Control-Allow-Origin: http://javascript.ru
|
||||
Access-Control-Allow-Credentials: true
|
||||
*/!*
|
||||
```
|
||||
|
||||
Использование звёздочки `*` в `Access-Control-Allow-Origin` при этом запрещено.
|
||||
|
||||
Если этих заголовков не будет, то браузер не даст JavaScript'у доступ к ответу сервера.
|
||||
|
||||
## "Непростые" запросы
|
||||
|
||||
В кросс-доменном `XMLHttpRequest` можно указать не только `GET/POST`, но и любой другой метод, например `PUT`, `DELETE`.
|
||||
|
||||
Когда-то никто и не думал, что страница сможет сделать такие запросы. Поэтому ряд веб-сервисов написаны в предположении, что "если метод -- нестандартный, то это не браузер". Некоторые веб-сервисы даже учитывают это при проверке прав доступа.
|
||||
|
||||
Чтобы пресечь любые недопонимания, браузер использует предзапрос в случаях, когда:
|
||||
|
||||
- Если метод -- не GET / POST / HEAD.
|
||||
- Если заголовок `Content-Type` имеет значение отличное от `application/x-www-form-urlencoded`, `multipart/form-data` или `text/plain`, например `application/xml`.
|
||||
- Если устанавливаются другие HTTP-заголовки, кроме `Accept`, `Accept-Language`, `Content-Language`.
|
||||
|
||||
...Любое из условий выше ведёт к тому, что браузер сделает два HTTP-запроса.
|
||||
|
||||
Первый запрос называется "предзапрос" (английский термин "preflight"). Браузер делает его целиком по своей инициативе, из JavaScript мы о нём ничего не знаем, хотя можем увидеть в инструментах разработчика.
|
||||
|
||||
Этот запрос использует метод `OPTIONS`. Он не содержит тела и содержит название желаемого метода в заголовке `Access-Control-Request-Method`, а если добавлены особые заголовки, то и их тоже -- в `Access-Control-Request-Headers`.
|
||||
|
||||
Его задача -- спросить сервер, разрешает ли он использовать выбранный метод и заголовки.
|
||||
|
||||
На этот запрос сервер должен ответить статусом 200, без тела ответа, указав заголовки `Access-Control-Allow-Method: метод` и, при необходимости, `Access-Control-Allow-Headers: разрешённые заголовки`.
|
||||
|
||||
Дополнительно он может указать `Access-Control-Max-Age: sec`, где `sec` -- количество секунд, на которые нужно закэшировать разрешение. Тогда при последующих вызовах метода браузер уже не будет делать предзапрос.
|
||||
|
||||

|
||||
|
||||
Давайте рассмотрим предзапрос на конкретном примере.
|
||||
|
||||
### Пример запроса COPY
|
||||
|
||||
Рассмотрим запрос `COPY`, который используется в протоколе [WebDAV](http://www.webdav.org/specs/rfc2518.html) для управления файлами через HTTP:
|
||||
|
||||
```js
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('COPY', 'http://site.com/~ilya', true);
|
||||
xhr.setRequestHeader('Destination', 'http://site.com/~ilya.bak');
|
||||
|
||||
xhr.onload = ...
|
||||
xhr.onerror = ...
|
||||
|
||||
xhr.send();
|
||||
```
|
||||
|
||||
Этот запрос "непростой" по двум причинам (достаточно было бы одной из них):
|
||||
|
||||
1. Метод `COPY`.
|
||||
2. Заголовок `Destination`.
|
||||
|
||||
Поэтому браузер, по своей инициативе, шлёт предварительный запрос `OPTIONS`:
|
||||
|
||||
```
|
||||
OPTIONS /~ilya HTTP/1.1
|
||||
Host: site.com
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
||||
Accept-Encoding: gzip,deflate
|
||||
Connection: keep-alive
|
||||
*!*
|
||||
Origin: http://javascript.ru
|
||||
Access-Control-Request-Method: COPY
|
||||
Access-Control-Request-Headers: Destination
|
||||
*/!*
|
||||
```
|
||||
|
||||
Обратим внимание на детали:
|
||||
|
||||
- Адрес -- тот же, что и у основного запроса: `http://site.com/~ilya`.
|
||||
- Стандартные заголовки запроса `Accept`, `Accept-Encoding`, `Connection` присутствуют.
|
||||
- Кросс-доменные специальные заголовки запроса:
|
||||
- `Origin` -- домен, с которого сделан запрос.
|
||||
- `Access-Control-Request-Method` -- желаемый метод.
|
||||
- `Access-Control-Request-Headers` -- желаемый "непростой" заголовок.
|
||||
|
||||
На этот запрос сервер должен ответить статусом 200, указав заголовки `Access-Control-Allow-Method: COPY` и `Access-Control-Allow-Headers: Destination`.
|
||||
|
||||
Но в протоколе WebDav разрешены многие методы и заголовки, которые имеет смысл сразу перечислить в ответе:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/plain
|
||||
*!*Access-Control-Allow-Methods*/!*: PROPFIND, PROPPATCH, COPY, MOVE, DELETE, MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, OPTIONS, GET, POST
|
||||
*!*Access-Control-Allow-Headers*/!*: Overwrite, Destination, Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control
|
||||
*!*Access-Control-Max-Age*/!*: 86400
|
||||
```
|
||||
|
||||
Ответ должен быть без тела, то есть только заголовки.
|
||||
|
||||
Браузер видит, что метод `COPY` -- в числе разрешённых и заголовок `Destination` -- тоже, и дальше он шлёт уже основной запрос.
|
||||
|
||||
При этом ответ на предзапрос он закэширует на 86400 сек (сутки), так что последующие аналогичные вызовы сразу отправят основной запрос, без `OPTIONS`.
|
||||
|
||||
Основной запрос браузер выполняет уже в "обычном" кросс-доменном режиме:
|
||||
|
||||
```
|
||||
COPY /~ilya HTTP/1.1
|
||||
Host: site.com
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
*!*
|
||||
Destination: http://site.com/~ilya.bak
|
||||
Origin: http://javascript.ru
|
||||
*/!*
|
||||
```
|
||||
|
||||
Ответ сервера, согласно спецификации [WebDav COPY](http://www.webdav.org/specs/rfc2518.html#rfc.section.8.8.8), может быть примерно таким:
|
||||
|
||||
```
|
||||
HTTP/1.1 207 Multi-Status
|
||||
Content-Type: text/xml; charset="utf-8"
|
||||
Content-Length: ...
|
||||
*!*
|
||||
Access-Control-Allow-Origin: http://javascript.ru
|
||||
*/!*
|
||||
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
...
|
||||
</d:multistatus>
|
||||
```
|
||||
|
||||
Так как `Access-Control-Allow-Origin` содержит правильный домен, то браузер вызовет `xhr.onload` и запрос будет завершён.
|
||||
|
||||
## Итого
|
||||
|
||||
- Все современные браузеры умеют делать кросс-доменные XMLHttpRequest.
|
||||
- В IE8,9 для этого используется объект `XDomainRequest`, ограниченный по возможностям.
|
||||
- Кросс-доменный запрос всегда содержит заголовок `Origin` с доменом запроса.
|
||||
|
||||
Порядок выполнения:
|
||||
|
||||
1. Для запросов с "непростым" методом или особыми заголовками браузер делает предзапрос `OPTIONS`, указывая их в `Access-Control-Request-Method` и `Access-Control-Request-Headers`.
|
||||
|
||||
Браузер ожидает ответ со статусом `200`, без тела, со списком разрешённых методов и заголовков в `Access-Control-Allow-Method` и `Access-Control-Allow-Headers`. Дополнительно можно указать `Access-Control-Max-Age` для кеширования предзапроса.
|
||||
2. Браузер делает запрос и проверяет, есть ли в ответе `Access-Control-Allow-Origin`, равный `*` или `Origin`.
|
||||
|
||||
Для запросов с `withCredentials` может быть только `Origin` и дополнительно `Access-Control-Allow-Credentials: true`.
|
||||
3. Если проверки пройдены, то вызывается `xhr.onload`, иначе `xhr.onerror`, без деталей ответа.
|
||||
4. Дополнительно: названия нестандартных заголовков ответа сервер должен указать в `Access-Control-Expose-Headers`, если хочет, чтобы клиент мог их прочитать.
|
||||
|
||||
Детали и примеры мы разобрали выше.
|
BIN
7-network/2-fetch-crossorigin/xhr-another-domain.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
7-network/2-fetch-crossorigin/xhr-another-domain@2x.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
7-network/2-fetch-crossorigin/xhr-preflight.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
7-network/2-fetch-crossorigin/xhr-preflight@2x.png
Normal file
After Width: | Height: | Size: 106 KiB |
163
7-network/3-fetch-api/article.md
Normal file
|
@ -0,0 +1,163 @@
|
|||
|
||||
# Fetch API
|
||||
|
||||
The second argument provides a lot of flexibility to `fetch` syntax.
|
||||
|
||||
Here's the full list of possible options with default values (alternatives commented out):
|
||||
|
||||
```js
|
||||
let promise = fetch(url, {
|
||||
method: "GET", // POST, PUT, DELETE, etc.
|
||||
headers: {
|
||||
"Content-Type": "text/plain;charset=UTF-8" // for a string body, depends on body
|
||||
},
|
||||
body: undefined // string, FormData, Blob, BufferSource, or URLSearchParams
|
||||
referrer: "about:client", // "" for no-referrer, or an url from the current origin
|
||||
referrerPolicy: "no-referrer-when-downgrade", // no-referrer, origin, same-origin...
|
||||
mode: "cors", // same-origin, no-cors, navigate, or websocket
|
||||
credentials: "same-origin", // omit, include
|
||||
cache: "default", // no-store, reload, no-cache, force-cache, or only-if-cached
|
||||
redirect: "follow", // manual, error
|
||||
integrity: "", // a hash, like "sha256-abcdef1234567890"
|
||||
keepalive: false, // true
|
||||
signal: undefined, // AbortController to abort request
|
||||
window: window // null
|
||||
});
|
||||
```
|
||||
|
||||
An impressive list, right? A lot of capabilities.
|
||||
|
||||
Let's explore what `fetch` can do.
|
||||
|
||||
We'll explore the options one-by-one with examples.
|
||||
|
||||
## method, headers, body
|
||||
|
||||
These are the most widely used fields.
|
||||
|
||||
- **`method`** -- HTTP-method, e.g. `POST`,
|
||||
- **`headers`** -- an object with HTTP headers,
|
||||
- **`body`** -- a string, or:
|
||||
- FormData object, to submit `form/multipart`
|
||||
- Blob/BufferSource to send binary data
|
||||
- URLSearchParams, to submit `x-www-form-urlencoded`
|
||||
|
||||
For instance, to submit a `user` object as JSON:
|
||||
|
||||
```js run async
|
||||
let user = {
|
||||
name: 'John',
|
||||
surname: 'Smith'
|
||||
};
|
||||
|
||||
*!*
|
||||
let response = await fetch('/article/fetch-api/post/user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
body: JSON.stringify(user)
|
||||
});
|
||||
*/!*
|
||||
|
||||
let result = await response.json();
|
||||
alert(JSON.stringify(result, null, 2));
|
||||
```
|
||||
|
||||
Please note, for a string body `Content-Type` is `text/plain;charset=UTF-8` by default. So the correct header is set manually.
|
||||
|
||||
Let's do the same with an HTML `<form>`:
|
||||
|
||||
```html run
|
||||
<form id="formElem">
|
||||
<input type="text" name="name" value="John">
|
||||
<input type="text" name="surname" value="Smith">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
fetch('/article/fetch-api/post/user', {
|
||||
method: 'POST',
|
||||
body: new FormData(formElem)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => alert(JSON.stringify(result, null, 2)))
|
||||
</script>
|
||||
```
|
||||
|
||||
Here [FormData](https://xhr.spec.whatwg.org/#formdata) automatically encodes the form (with files if any), and the `Content-Type` is `form/multipart`.
|
||||
|
||||
We can also submit binary data directly using `Blob` or `BufferSource`.
|
||||
|
||||
For example, here's a `<canvas>` where we can draw by moving a mouse. A click on the "submit" button sends the image to server:
|
||||
|
||||
```html run autorun height="90"
|
||||
<body style="margin:0">
|
||||
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
|
||||
|
||||
<input type="button" value="Submit" onclick="submit()">
|
||||
|
||||
<script>
|
||||
canvasElem.onmousemove = function(e) {
|
||||
let ctx = canvasElem.getContext('2d');
|
||||
ctx.lineTo(e.clientX, e.clientY);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
function submit() {
|
||||
canvasElem.toBlob(function(blob) {
|
||||
fetch('/article/fetch-api/post/image', {
|
||||
method: 'POST',
|
||||
body: blob
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => alert(JSON.stringify(result, null, 2)))
|
||||
}, 'image/png');
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
|
||||
Here we also didn't need to set `Content-Type` manually, because a `Blob` object has a built-in type (here `image/png`, as generated by `toBlob`).
|
||||
|
||||
```warn header="Not any header is allowed"
|
||||
There's a list of [forbidden HTTP headers](https://fetch.spec.whatwg.org/#forbidden-header-name) that we can't set:
|
||||
|
||||
- `Accept-Charset`, `Accept-Encoding`
|
||||
- `Access-Control-Request-Headers`
|
||||
- `Access-Control-Request-Method`
|
||||
- `Connection`
|
||||
- `Content-Length`
|
||||
- `Cookie`, `Cookie2`
|
||||
- `Date`
|
||||
- `DNT`
|
||||
- `Expect`
|
||||
- `Host`
|
||||
- `Keep-Alive`
|
||||
- `Origin`
|
||||
- `Referer`
|
||||
- `TE`
|
||||
- `Trailer`
|
||||
- `Transfer-Encoding`
|
||||
- `Upgrade`
|
||||
- `Via`
|
||||
- `Proxy-*`
|
||||
- `Sec-*`
|
||||
|
||||
All these headers are controlled solely by the browser, to ensure proper and safe HTTP. So we can't set them.
|
||||
```
|
||||
|
||||
## referrer, referrerPolicy
|
||||
|
||||
These options govern how `fetch` sets HTTP `Referer` header (header has one `r`, for historical reasons).
|
||||
|
||||
That header tells the server which page made the request. In most scenarios, it plays a very minor informational role, so if you don't have specific reasons to care about it, like increased security, please skip to the next section.
|
||||
|
||||
- **`"no-referrer-when-downgrade"`** -- default value, the referer is set always, unless we send a request from HTTPS to HTTP (to less secure protocol).
|
||||
- **`"no-referrer"`** -- don't set the referer.
|
||||
- **`"origin"`** -- only set the domain, not the full page URL, e.g. `http://site.com` instead of `http://site.com/path`.
|
||||
|
||||
no-referrer
|
||||
The Referer header will be omitted entirely. No referrer information is sent along with requests.
|
||||
(default)
|
||||
This is the user agent's default behavior if no policy is specified. The URL is sent as a referrer when the protocol security level stays the same (HTTP→HTTP, HTTPS→HTTPS), but isn't sent to a less secure destination (HTTPS→HTTP).
|
4
7-network/3-fetch-api/logo-fetch.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="45" fill="#fff" stroke="#3c790a" stroke-width="10"/>
|
||||
<path d="m34,55a60,60,0,0,0,20,-20a6,10,0,0,1,13,-1a10,6,0,0,1,-1,13a60,60,0,0,0,-20,20a6,10,0,0,1,-13,1a10,6,0,0,1,1,-13" fill="#3c790a"/>
|
||||
</svg>
|
After Width: | Height: | Size: 290 B |
36
7-network/3-fetch-api/post.view/index.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!doctype html>
|
||||
<script>
|
||||
(async () {
|
||||
|
||||
const response = await fetch('long.txt');
|
||||
const reader = response.body.getReader();
|
||||
|
||||
const contentLength = +response.headers.get('Content-Length');
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while(true) {
|
||||
const chunk = await reader.read();
|
||||
|
||||
if (chunk.done) {
|
||||
console.log("done!");
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(chunk.value);
|
||||
receivedLength += chunk.value.length;
|
||||
console.log(`${receivedLength}/${contentLength} received`)
|
||||
}
|
||||
|
||||
|
||||
let chunksMerged = new Uint8Array(receivedLength);
|
||||
let length = 0;
|
||||
for(let chunk of chunks) {
|
||||
chunksMerged.set(chunk, length);
|
||||
length += chunk.length;
|
||||
}
|
||||
|
||||
let result = new TextDecoder("utf-8").decode(chunksMerged);
|
||||
console.log(result);
|
||||
})();
|
||||
|
||||
</script>
|
55
7-network/3-fetch-api/post.view/server.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
let http = require('http');
|
||||
let url = require('url');
|
||||
let querystring = require('querystring');
|
||||
let static = require('node-static');
|
||||
let file = new static.Server('.', {
|
||||
cache: 0
|
||||
});
|
||||
|
||||
|
||||
function accept(req, res) {
|
||||
|
||||
if (req.method == 'POST') {
|
||||
let chunks = [];
|
||||
let length = 0;
|
||||
|
||||
req.on('data', function (data) {
|
||||
chunks.push(data);
|
||||
length += data.length;
|
||||
|
||||
// Too much POST data, kill the connection!
|
||||
if (length > 1e6) {
|
||||
request.connection.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
req.on('end', function() {
|
||||
// let post = JSON.parse(chunks.join(''));
|
||||
|
||||
if (req.url == '/user') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: 'User saved' }));
|
||||
} else if (req.url == '/image') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: "Image saved", imageSize: length }));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
file.serve(req, res);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ------ запустить сервер -------
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
}
|
41
7-network/5-fetch-progress/article.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
|
||||
# Fetch API
|
||||
|
||||
The second argument provides a lot of flexibility to `fetch` syntax.
|
||||
|
||||
Here's the full list of possible options with default values (alternatives commented out):
|
||||
|
||||
```js
|
||||
let promise = fetch(url, {
|
||||
method: "GET", // POST, PUT, DELETE, etc.
|
||||
headers: {
|
||||
"Content-Type": "text/plain;charset=UTF-8"
|
||||
},
|
||||
body: undefined // string, FormData, Blob, BufferSource, or URLSearchParams
|
||||
referrer: "about:client", // "" for no-referrer, or an url from the current origin
|
||||
referrerPolicy: "", // no-referrer, no-referrer-when-downgrade, same-origin...
|
||||
mode: "cors", // same-origin, no-cors, navigate, or websocket
|
||||
credentials: "same-origin", // omit, include
|
||||
cache: "default", // no-store, reload, no-cache, force-cache, or only-if-cached
|
||||
redirect: "follow", // manual, error
|
||||
integrity: "", // a hash, like "sha256-abcdef1234567890"
|
||||
keepalive: false, // true
|
||||
signal: undefined, // AbortController to abort request
|
||||
window: window // null
|
||||
})
|
||||
```
|
||||
|
||||
Not so long list actually, but quite a lot of capabilities.
|
||||
|
||||
Let's explore the options one-by-one with examples.
|
||||
|
||||
## method, headers, body
|
||||
|
||||
These are the most widely used fields.
|
||||
|
||||
- **`method`** -- HTTP-method, e.g. POST,
|
||||
- **`headers`** -- an object with HTTP headers,
|
||||
- **`body`** -- a string, or:
|
||||
- FormData object, to submit `form/multipart`
|
||||
- Blob/BufferSource to send binary data
|
||||
- URLSearchParams, to submit `x-www-form-urlencoded`
|
4
7-network/5-fetch-progress/logo-fetch.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="45" fill="#fff" stroke="#3c790a" stroke-width="10"/>
|
||||
<path d="m34,55a60,60,0,0,0,20,-20a6,10,0,0,1,13,-1a10,6,0,0,1,-1,13a60,60,0,0,0,-20,20a6,10,0,0,1,-13,1a10,6,0,0,1,1,-13" fill="#3c790a"/>
|
||||
</svg>
|
After Width: | Height: | Size: 290 B |
36
7-network/5-fetch-progress/progress.view/index.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!doctype html>
|
||||
<script>
|
||||
(async () {
|
||||
|
||||
const response = await fetch('long.txt');
|
||||
const reader = response.body.getReader();
|
||||
|
||||
const contentLength = +response.headers.get('Content-Length');
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while(true) {
|
||||
const chunk = await reader.read();
|
||||
|
||||
if (chunk.done) {
|
||||
console.log("done!");
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(chunk.value);
|
||||
receivedLength += chunk.value.length;
|
||||
console.log(`${receivedLength}/${contentLength} received`)
|
||||
}
|
||||
|
||||
|
||||
let chunksMerged = new Uint8Array(receivedLength);
|
||||
let length = 0;
|
||||
for(let chunk of chunks) {
|
||||
chunksMerged.set(chunk, length);
|
||||
length += chunk.length;
|
||||
}
|
||||
|
||||
let result = new TextDecoder("utf-8").decode(chunksMerged);
|
||||
console.log(result);
|
||||
})();
|
||||
|
||||
</script>
|
26973
7-network/5-fetch-progress/progress.view/long.txt
Normal file
31
7-network/6-url/progress.view/index.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!doctype html>
|
||||
<script>
|
||||
|
||||
async function init() {
|
||||
const response = await fetch('long.txt');
|
||||
const reader = response.body.getReader();
|
||||
|
||||
const contentLength = +response.headers.get('Content-Length');
|
||||
let receivedLength = 0;
|
||||
|
||||
while(true) {
|
||||
const chunk = await reader.read();
|
||||
|
||||
if (chunk.done) {
|
||||
console.log("done!");
|
||||
break;
|
||||
}
|
||||
|
||||
receivedLength += chunk.value.length;
|
||||
console.log(`${receivedLength}/${contentLength} received`)
|
||||
}
|
||||
|
||||
let result = await response.text();
|
||||
console.log(result);
|
||||
//const chunkCount = await read(reader);
|
||||
//console.log(`Finished! Received ${chunkCount} chunks.`);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
</script>
|