JavaRush /Курсы /Java Server /Конфигурация HttpRequest

Конфигурация HttpRequest

Java Server
15 уровень , 1 лекция
Открыта

1. HttpRequest как «паспорт» запроса

Когда новичок пишет HTTP-клиент, он часто думает так: «Я же уже знаю URL, значит запрос почти готов». Но в реальности URL — это только адрес. Чтобы запрос стал запросом, нам нужно зафиксировать ещё метод, заголовки и, если это не GET, то и body. В Java объект HttpRequest как раз и выступает «паспортом» вызова: в нём явно записано, что мы просим у сервера и в каком виде.

Если смотреть на это «с высоты птичьего полёта», то HttpRequest — это то, что вы сначала аккуратно собираете, а потом отдаёте в client.send(...). Поэтому чем читабельнее выглядит сборка запроса, тем меньше у вас загадок уровня «почему сервер меня не понимает».

Небольшая схема (без попыток стать учебником RFC):

flowchart TD
    %% Важно: сеть появляется только на этапе send, до этого мы просто "собираем паспорт" запроса
    A["URI (куда идём)"] --> B["HttpRequest.Builder (как именно идём)"]
    B --> C["HttpRequest (зафиксированная конфигурация)"]
    C --> D["HttpClient.send(...) (попытка сходить в сеть)"]
    D --> E["HttpResponse (status + headers + body)"]

Обратите внимание: сеть появляется только на этапе send. Всё, что до этого — наши решения, и их удобно видеть прямо в коде.

Минимальный «скелет» запроса выглядит так:

import java.net.URI;
import java.net.http.HttpRequest;

// Куда идём: это только адрес, не "готовый запрос"
URI uri = URI.create("https://catalog.example/search?q=java");

// Собираем "паспорт" запроса: метод + (потом при необходимости) заголовки и body
HttpRequest request = HttpRequest.newBuilder(uri)
        .GET()   // Явно фиксируем метод
        .build(); // После build() запрос становится неизменяемым

Да, это уже запрос. Но пока он слишком «немой»: в нём нет договорённости о формате ответа, и в реальном проекте такие запросы быстро превращаются в «работает… пока работает».

2. HttpRequest.Builder: сборка запроса

В HttpClient-API очень удачно сделано то, что новичку обычно нравится: запрос собирается через builder, а значит его можно читать сверху вниз как небольшой сценарий. В каждой строке вы как будто отвечаете на вопрос: куда идём, что ждём в ответ, чем (если надо) нагружаем запрос, какой метод выбираем. Это особенно полезно в учебном проекте, где мы специально не прячем механику за фреймворками.

Важный момент: HttpRequest после build() становится неизменяемым. Это означает, что запрос «заморожен» и случайно его не испортить. А вот HttpRequest.Builder — штука временная: собрали, построили, забыли. Не пытайтесь «переиспользовать builder на все случаи жизни», иначе довольно быстро поймаете неочевидную кашу: у вас в одном месте добавили заголовок, а он внезапно «прилепился» к другому запросу.

Типичная читабельная сборка GET-запроса в нашем стиле:

import java.net.URI;
import java.net.http.HttpRequest;

URI uri = URI.create("https://catalog.example/search?q=clean+code");

HttpRequest request = HttpRequest.newBuilder(uri)
        .header("Accept", "application/json") // Договариваемся о формате ответа
        .GET()                                // Фиксируем метод
        .build();                             // "Замораживаем" конфигурацию запроса

Здесь уже видно три вещи: куда идём, что хотим получить, каким методом идём. По сути, это и есть наш маленький «контракт» клиента с сервером, только со стороны клиента.

3. GET: header Accept и «вежливое» ожидание JSON-ответа

До этого момента мы часто работали с ответом как с сырой строкой, и это нормально для старта. Но даже когда мы читаем body как строку, нам всё равно важно договориться с сервером, какой формат ответа мы ожидаем. В HTTP для этого есть заголовок Accept. Он означает: «Сервер, пожалуйста, если ты умеешь, ответь мне в таком формате».

В реальности некоторые API всегда отвечают JSON и игнорируют Accept. Но в учебном проекте нам важна привычка: Accept — часть нормального HTTP-контракта. Плюс бывают API, которые умеют отвечать и JSON, и чем-нибудь ещё (например, HTML или XML), и там Accept реально решает, что вы получите.

Давайте продолжим наш сценарий поиска книг в каталоге. Предположим, мы уже собрали URI (из лекции 1), и теперь делаем запрос:

import java.net.URI;
import java.net.http.HttpRequest;

// В URI уже зашиты query-параметры (это часть адреса)
URI uri = URI.create("https://catalog.example/search?q=clean+code&limit=5");

HttpRequest request = HttpRequest.newBuilder(uri)
        .header("Accept", "application/json") // Просим JSON (если сервер умеет)
        .GET()                                // GET без body — нормальный сценарий
        .build();

Обратите внимание: мы не ставим Content-Type для GET. Почему? Потому что Content-Type описывает тело запроса, а в GET тела обычно нет. Да, бывают экзотические случаи «GET с body», но если вы не хотите страдать — не надо. Мы строим предсказуемый клиент, а не ребус.

Иногда полезно добавить ещё и User-Agent. Некоторые внешние сервисы относятся к нему капризно, а иногда по нему проще диагностировать, что запрос пришёл именно от нашего приложения. Но не превращайте заголовки в коллекцию марок: добавляем только то, что понимаем.

Например, вот так:

import java.net.URI;
import java.net.http.HttpRequest;

URI uri = URI.create("https://catalog.example/search?q=java");

HttpRequest request = HttpRequest.newBuilder(uri)
        .header("Accept", "application/json")           // Что хотим получить
        .header("User-Agent", "readlater-starter/1.0")  // Кто мы такие (для диагностики и капризных API)
        .GET()
        .build();

4. Accept и Content-Type: различия

Путаница Accept и Content-Type — это почти обязательный «ритуал посвящения» в HTTP. Ничего страшного: у этих заголовков действительно похожий вид, но смысл у них разный. Чтобы перестать путать, удобно держать в голове простую идею: один заголовок говорит про то, что вы хотите получить, а другой — про то, что вы отправляете.

Можно даже представить диалог:

- Accept: «Я умею воспринимать JSON, пожалуйста, ответь JSON-ом».
- Content-Type: «Я отправляю тебе JSON. Не думай, что это plain text или что-то ещё».

И это работает и для запросов, и для ответов. В ответе сервера Content-Type означает: «Вот в каком формате лежит body ответа».

Сведём различия в короткую табличку (она реально экономит время на старте):

Заголовок Где встречается О чём он Пример
Accept в запросе клиента какой формат ответа клиент предпочитает/умеет Accept: application/json
Content-Type в запросе клиента в каком формате client отправляет body Content-Type: application/json
Content-Type в ответе сервера в каком формате server возвращает body Content-Type: application/json; charset=utf-8

Если хочется совсем бытовую аналогию, то Accept — это как фраза «мне, пожалуйста, кофе без молока», а Content-Type — это наклейка на контейнере «внутри суп». Вроде оба про еду, но один про ожидание, другой про фактическое содержимое.

5. POST с JSON-body: BodyPublishers.ofString

POST мы сегодня используем в учебных целях, потому что на реальных каталогах книг поиск часто делается GET-ом с query-параметрами. Но отправка JSON-body — это настолько частая история в backend-интеграциях, что пройти мимо неё нельзя. Даже если сегодня вы не будете реально слать POST в наш внешний каталог, механика должна стать знакомой: метод + headers + body.

В HttpRequest body задаётся через BodyPublisher. Самый простой вариант — отправить строку, то есть BodyPublishers.ofString(...). И вот здесь появляется два практичных нюанса: JSON-строку нужно собрать без ошибок, и нужно выставить Content-Type: application/json.

В Java 25 нам очень помогают text blocks. Они экономят нервы, потому что не нужно экранировать каждую кавычку в JSON. Вместо вот такого «кавычечного ада» мы пишем почти нормальный JSON.

Пример «учебного запроса», который отправляет JSON на условный endpoint /preview:

import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.nio.charset.StandardCharsets;

String baseUrl = "https://catalog.example";

// Тело запроса: JSON удобно держать как text block, чтобы не экранировать кавычки
String json = """
        {
          "query": "clean code",
          "limit": 5
        }
        """;

HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + "/preview"))
        .header("Accept", "application/json")        // Хотим получить JSON-ответ
        .header("Content-Type", "application/json")  // Сообщаем, что мы отправляем JSON
        .POST(BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // Фиксируем method + body + кодировку
        .build();

Здесь важно увидеть «костяк» POST-запроса глазами:

1) POST(...) делает запрос именно POST-ом и одновременно задаёт body.
2) Content-Type говорит серверу, что внутри body JSON.
3) Accept говорит серверу, что мы хотим получить JSON в ответ.

Если вы забудете Content-Type, некоторые серверы будут пытаться интерпретировать body как «не пойми что», и вы получите ошибку, которая выглядит как «сервер сломан». На самом деле он просто не понял, что вы ему отправили.

Ещё один маленький момент: многие API ожидают Content-Type: application/json; charset=utf-8. В современном мире JSON почти всегда в UTF‑8, и многие сервера понимают это по умолчанию. Но если вы видите странные проблемы с кириллицей или спецсимволами, имеет смысл указывать charset явно. В рамках наших примеров мы оставим application/json ради простоты, а кодировку обеспечим через ofString(...).

6. Повторяющиеся заголовки: маленький helper

Когда вы делаете два-три запроса, всё кажется простым. Но как только появляется «поиск» и «детали», а потом ещё парочка вариантов, вы внезапно обнаруживаете, что в каждом месте копируете одно и то же: Accept, иногда User-Agent, иногда Content-Type. Копипаст в transport-коде опасен не философски, а очень практично: одна строка где-то уехала — и у вас «почему-то один запрос работает, а другой нет».

При этом мы не хотим строить «универсальный фреймворк запросов». Нам нужен маленький, честный helper, чтобы сборка запроса стала короче, но не потеряла понятность.

Простейший вариант — метод, который возвращает настроенный HttpRequest.Builder под JSON-ответы:

import java.net.URI;
import java.net.http.HttpRequest;

private static HttpRequest.Builder jsonRequest(URI uri) {
    return HttpRequest.newBuilder(uri)
            .header("Accept", "application/json")          // Базовая договорённость: хотим JSON
            .header("User-Agent", "readlater-starter/1.0");// Базовая идентификация клиента
}

Тогда GET становится совсем читабельным:

import java.net.URI;
import java.net.http.HttpRequest;

URI uri = URI.create("https://catalog.example/search?q=java");

HttpRequest request = jsonRequest(uri) // Берём базовые заголовки (Accept + User-Agent)
        .GET()                         // Добавляем метод для конкретного запроса
        .build();

А POST — тоже:

import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.nio.charset.StandardCharsets;

// Короткий JSON тоже можно держать как text block, чтобы не возиться с экранированием
String json = """
        {
          "query": "java",
          "limit": 3
        }
        """;

HttpRequest request = jsonRequest(URI.create("https://catalog.example/preview"))
        .header("Content-Type", "application/json") // Для POST с body это важно: что именно мы отправляем
        .POST(BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // Тело + кодировка
        .build();

Заметьте, мы не пытаемся прятать «POST с body» куда-то в глубины. Мы просто убрали повторяющиеся куски, а смысл запроса по-прежнему читается в 5–6 строк.

7. Типичные ошибки при сборке HttpRequest

Ошибка №1: путать Accept и Content-Type.
Самая частая история выглядит так: вы делаете POST, выставляете Accept: application/json, думаете «ну я же про JSON сказал», и удивляетесь, почему сервер ругается на тело. Серверу всё равно, что вы хотите получить в ответ; ему нужно понять, что вы отправили. Для этого и существует Content-Type.

Ошибка №2: отправить POST без body (или с пустым body) случайно.
Иногда хочется написать просто .POST(...), но «что туда передать» непонятно, и рука тянется к чему-нибудь вроде BodyPublishers.noBody(). Формально это возможно, но в JSON-сценариях обычно бессмысленно: сервер ждал JSON, а вы отправили «ничего». Если сценарий действительно про JSON, то body должен быть явным.

Ошибка №3: собрать JSON строкой с кавычками и сломать её на ровном месте.
Строка вида "{\"query\":\"clean code\"}" выглядит как наказание за грехи из прошлой жизни. В ней легко забыть обратный слэш, закрыть кавычку не там и получить «невалидный JSON». В учебном проекте лучше либо использовать Java text block (""" ... """), либо держать JSON коротким и проверяемым.

Ошибка №4: выставить Content-Type на GET «на всякий случай».
Content-Type описывает тело запроса. В GET тела нет, и заголовок становится странным шумом. Иногда серверы терпеливы, но иногда вы попадаете на API, которое делает строгую проверку и начинает вести себя непредсказуемо. «На всякий случай» в HTTP — это частый путь к неожиданностям.

Ошибка №5: пытаться переиспользовать один HttpRequest.Builder для разных запросов.
Builder удобно выглядит как «объект-конфигуратор», и возникает мысль: «создам один, а потом буду менять URI». На практике вы быстро забываете, какие заголовки вы уже добавили, и получаете случайный набор headers в разных запросах. Создавайте builder заново на каждый запрос, а общий минимум выносите в маленький helper (как мы сделали выше).

1
Задача
Java Server, 15 уровень, 1 лекция
Недоступна
Конфигурация GET-запроса без отправки в сеть
Конфигурация GET-запроса без отправки в сеть
1
Задача
Java Server, 15 уровень, 1 лекция
Недоступна
POST-запрос с JSON-body
POST-запрос с JSON-body
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ