От Go API к вызову другой программы

Пустой ответ, 503, обрыв и повреждённый JSON

Содержание курса

Четыре ответа без внешнего сервера

Чтобы пройти по всем четырём веткам probe, не нужно запускать и останавливать реальный сервер. http.Client отправляет запрос не напрямую в сеть, а через поле Transport типа http.RoundTripper. Это интерфейс с единственным методом:

Четыре разных исхода HTTP-чтения. Полученный HTTP-статус, транспортная ошибка и ошибка декодирования — разные наблюдения.
Полученный HTTP-статус, транспортная ошибка и ошибка декодирования — разные наблюдения.

Статичная схема (PNG)

RoundTrip(*http.Request) (*http.Response, error)

Если подставить собственную реализацию, client.Do останется настоящим вызовом Go HTTP-клиента, но внешнее TCP-соединение не создаётся. Вместо него срабатывает ваша функция и возвращает готовый *http.Response или ошибку. Здесь подменный транспорт возвращает результат сразу и не проверяет отмену. Чтобы моделировать ожидание и отмену, собственный RoundTripper должен учитывать r.Context().Done().

В коде ниже roundTripFunc — это тип-адаптер: функция с нужной сигнатурой автоматически становится RoundTripper через метод RoundTrip. Вспомогательная response собирает минимальный *http.Response с нужным статусом и строкой в теле через io.NopCloser, чтобы тело корректно реализовывало io.ReadCloser.

Для воспроизведения создайте один main.go. Поместите в его начало следующий блок, затем функцию probe из предыдущего шага и код ниже. Вместе они образуют полную программу; отдельный файл к уроку не прикреплён.


package main

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "strings"
)

type Article struct {
    ID    string `json:"id"`
    Title string `json:"title"`
}


type roundTripFunc func(*http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
    return f(r)
}

func response(status int, body string) *http.Response {
    return &http.Response{
        StatusCode: status,
        Body:       io.NopCloser(strings.NewReader(body)),
        Header:     make(http.Header),
    }
}

func main() {
    cases := []struct {
        name string
        do   roundTripFunc
    }{
        {"empty", func(*http.Request) (*http.Response, error) {
            return response(200, "[]"), nil
        }},
        {"unavailable", func(*http.Request) (*http.Response, error) {
            return response(503, `{"error":"temporarily unavailable"}`), nil
        }},
        {"connection", func(*http.Request) (*http.Response, error) {
            return nil, errors.New("connection refused")
        }},
        {"broken JSON", func(*http.Request) (*http.Response, error) {
            return response(200, "["), nil
        }},
    }
    for _, tc := range cases {
        client := &http.Client{Transport: tc.do}
        fmt.Printf("%s: %s\n", tc.name, probe(context.Background(), client))
    }
}

В main каждый тест-кейс задаёт свою roundTripFunc. Разберём, какая ветка probe срабатывает в каждом случае.

  • empty — транспорт возвращает (response(200, "[]"), nil). Do получает готовый ответ без ошибки. В probe первая проверка err != nil не срабатывает, вторая StatusCode != 200 тоже. Декодер читает [] и создаёт пустой срез. Результат: articles=0.

  • unavailable — транспорт возвращает (response(503, …), nil). Do снова успешен. В probe ветка err != nil пропускается, зато StatusCode != 200 истинна — функция возвращает HTTP 503 и выходит. Тело закрыто через defer.

  • connection — транспорт возвращает (nil, errors.New("connection refused")). Do передаёт эту ошибку наружу. В probe первая же проверка err != nil истинна — функция немедленно возвращает transport error: …. К resp обращений нет.

  • broken JSON — транспорт возвращает (response(200, "["), nil). HTTP-обмен прошёл, Do вернул resp без ошибки. В probe обе первые проверки проходят. Декодер начинает читать незакрытый JSON-массив [ и обнаруживает конец потока раньше закрывающей скобки — возвращает unexpected EOF. Ветка JSON error срабатывает уже после завершения HTTP.

Команда go run main.go для собранного файла печатает:


empty: HTTP 200, articles=0
unavailable: HTTP 503
connection: transport error: Get "http://articles.test/articles": connection refused
broken JSON: JSON error: unexpected EOF

Читайте трассу слева направо: только connection прерывается на Do; broken JSON доходит до декодера после успешного HTTP-обмена. Именно это и делает подменный транспорт полезным инструментом: обе «поздние» ветки — 503 и сломанный JSON — воспроизводятся точно и предсказуемо, без остановки и перезапуска сервера.

Начать заново?

Помощник начнёт новый диалог по этой задаче. Текущая переписка останется в истории до конца срока хранения.

О переписке

Можно приложить до трёх фото PNG, JPEG или WebP по 5 МБ.

Переписка и фото хранятся 90 дней после последнего принятого сообщения. Преподаватель получит доступ к диалогу, когда вы попросите его помочь.

Проверка решения остаётся отдельным действием.