Статьи

100 Продолжить поддержку в Play

Код состояния  100 Continue  в спецификации HTTP — это тот, о котором большинство людей знают очень мало. Вы как бы читаете это, не понимаете, о чем идет речь, а затем просто пропускаете это. Я не знал, о чем идет речь, пока не стал разработчиком веб-фреймворка. Оказывается, очень полезно в определенных ситуациях.

Допустим, клиент должен сделать очень большую загрузку, например, 1 ГБ. Что произойдет, если сервер не сможет удовлетворить запрос клиента? Например, что если клиент отправил неверные учетные данные для аутентификации? Или содержание запроса было слишком длинным? Или неправильный тип носителя? HTTP — полудуплексный протокол, клиент и сервер принимают его по очереди, чтобы говорить. Это означает, что, хотя сервер может сразу же после получения заголовка запроса узнать, что он не может обработать запрос, ему все равно необходимо прочитать все тело запроса, прежде чем он сможет сообщить об этом клиенту, даже если это тело имеет длину 1 ГБ. и занимает час, чтобы загрузить. И если вы когда-либо делали какие-либо большие HTTP-загрузки ранее, вы будете знать, что нет ничего более разочаровывающего, чем дойти до конца большой загрузки, только получить сообщение об ошибке с сервера.

HTTP имеет решение этой проблемы в форме   заголовка запроса Expect . Заголовок Expect используется, чтобы сообщить серверу, что клиент ожидает от него определенного поведения. Для него есть одно определенное значение в спецификации HTTP, а это  100-продолжение . Это сообщает серверу, что после отправки заголовков запроса клиент не будет отправлять тело запроса до тех пор, пока не получит ответ 100 continue. В противном случае сервер может немедленно вернуться с любым другим кодом ответа. После получения ответа 100 continue клиент продолжит отправку тела, и после того, как сервер потребит его, сервер отправит второй ответ.

Это может использоваться всякий раз, когда сервер хочет выполнить проверку только заголовков запроса. Вот некоторые примеры:

  • Аутентификация — если клиент не аутентифицирован, сервер может ответить 401 Unauthorized.
  • Авторизация — если клиент не авторизован для выполнения запроса, сервер может ответить 403 Запрещено.
  • Существование ресурса — если клиент попытался поместить ресурс в несуществующее место, сервер может ответить 404 Not Found
  • Ограничения длины контента — если клиент не отправил длину контента, сервер может ответить 411 Длина обязательна, или если длина контента больше, чем сервер готов принять, сервер может ответить 413 Request Entity Too Large
  • Проверка типа контента — если клиент отправляет тип контента, который сервер не поддерживает, сервер может ответить 415 Unsupported Media Type

100 продолжают поддержку в Play Framework

Итак, учитывая все это, как это можно реализовать в платформе Play? Как вы, возможно, знаете, на самом низком уровне действие Play выглядит следующим образом:

trait EssentialAction extends (RequestHeader => Iteratee[Array[Byte], Result])

Итератор, который возвращает основная функция действия, — это то, что потребляет тело. Итератор может находиться в одном из трех состояний: выполнено, продолжено (готово к приему дополнительных входных данных) или ошибка. Когда Play вызывает действие, чтобы получить итерируемого для тела, и клиент указал заголовок Expect: 100-continue, Play может проверить, готов ли этот итератор к получению ввода, или он находится в состоянии «сделано» или «ошибка». Если он находится в состоянии «сделано» или «ошибка», Play немедленно отправит результат, не используя тело. Если он находится в состоянии cont, Play отправит ответ 100 continue, а затем направит тело в итерируемый.

Таким образом, чтобы действие могло использовать это в своих интересах, нужно просто убедиться, что оно возвращает готового итератора в случае сбоя проверки. Воспроизведение встроенного действия аутентификации делает именно это:

def Authenticated[A](
  userinfo: RequestHeader => Option[A],
  onUnauthorized: RequestHeader => Result)(action: A => EssentialAction): EssentialAction = {
 
  EssentialAction { request =>
    userinfo(request).map { user =>
      action(user)(request)
    }.getOrElse {
      Done(onUnauthorized(request), Input.Empty)
    }
  }
}

Кроме того, все парсеры тела Plays при проверке типа контента возвращают готового итератора, если тип контента неправильный. Итак, если у меня есть действие, которое выглядит так:

def upload = Authenticated(
    rh => rh.headers.get("Authentication-Token").filter(_ == "secret-token"), 
    rh => Forbidden("Authentication required")
) { token => Action(parse.text) { request =>
  Ok("Got body that was " + request.body.length + " characters long")
}}

И тогда я отправляю следующий заголовок запроса:

POST /upload HTTP/1.1
Host: localhost
Authentication-Token: secret-token
Content-Type: text/plain
Content-Length: 12
Expect: 100-continue

Игра немедленно ответит:

100 Continue HTTP/1.1

В этот момент я могу отправить свое тело, и Play отправит ответ. Вся транзакция будет выглядеть так:


C: POST /upload HTTP/1.1 C: Host: localhost C: Authentication-Token: secret-token C: Content-Type: text/plain C: Content-Length: 12 C: Expect: 100-continue C: S: HTTP/1.1 100 Continue S: C: Hello world! S: HTTP/1.1 200 OK S: Content-Type: text/plain;charset=utf-8 S: Content-Length: 37 S: S: Got body that was 12 characters long

Однако, если я не отправлю токен аутентификации или если мой тип контента будет неправильным, это то, что произойдет:

C: POST /upload HTTP/1.1
C: Host: localhost
C: Content-Type: text/plain
C: Content-Length: 12
C: Expect: 100-continue
C: 
S: HTTP/1.1 403 Forbidden
S: Content-Type: text/plain;charset=utf-8
S: Content-Length: 23
S:
S: Authentication required

И поэтому, хотя в заголовке запроса я сказал, что длина контента равна 12, мне не нужно было загружать его, потому что я отправил ожидаемый заголовок, а Play не отправил ответ 100 continue, вместо этого он смог немедленно скажите мне, что запрос потерпит неудачу. Очевидно, что при таком маленьком теле это не имеет особого смысла, но при длине тела в гигабайты это означает, что мне не нужно тратить столько часов на его загрузку, прежде чем я наконец узнаю, что мне не разрешили загрузить его.