---
title: "OnRequest и OnResponse: запросы под контролем"
description: "colly onrequest и colly onresponse: аргументы Request и Response, порядок колбэков от Visit до OnScraped, Abort для лишних URL и сохранение HTML через Save."
course: "Colly: парсинг и веб-скрапинг на Go"
url: "https://golangmaster.ru/courses/colly/lessons/09-onrequest-onresponse/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# OnRequest и OnResponse: запросы под контролем

*Курс «Colly: парсинг и веб-скрапинг на Go», урок 9 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/colly/lessons/09-onrequest-onresponse/*

Обходчик из прошлого урока уже ходит по каталогу сам, но за кадром остаётся главное: куда именно ушли запросы и что вернулось. Colly onrequest и colly onresponse — два колбэка, которые показывают каждый шаг: адрес перед отправкой, статус и тело после загрузки. Разберём их аргументы, порядок вызовов и Abort для отсечения мусора до сети.

До сих пор наш скрапер был чёрным ящиком: c.Visit со стартовой страницы — и через мгновение OnHTML уже перебирает карточки товаров. Между этими двумя строками colly успевает построить HTTP-запрос, отправить его, получить заголовки, скачать тело и отдать его парсеру — и весь этот конвейер остаётся невидимым, пока вы сами не подпишетесь на его этапы. Невидимость дорого стоит. Когда сайт отвечает 403, вы узнаёте об этом по пустому выводу, а не по логу. Когда каталог редиректит на страницу входа, скрапер честно «работает»: разбирает страницу логина, ничего там не находит и молчит — вы тратите вечер на перечитывание селекторов, хотя проблема случилась на два шага раньше. А когда обход тянет картинки и архивы, вы платите трафиком и временем, потому что отменять неучтённые запросы негде. В этом уроке вешаем датчики на каждый этап конвейера: увидим запрос до отправки, ответ после загрузки, точку между заголовками и телом — и научимся отсекать мусор до того, как он уйдёт в сеть.

### colly onrequest: последняя точка перед отправкой

Регистрируется обработчик методом OnRequest с сигнатурой func(r *colly.Request), и вызывается colly в последний момент перед уходом запроса в сеть — когда адрес уже разобран, глубина посчитана, очередь дала добро. В руках у вас весь запрос целиком. r.URL — это *url.URL с хостом, путём и query-параметрами: от проверки префикса пути до разбора расширения файла доступно без строковых фокусов. r.Method говорит, что предстоит — GET или POST. r.Depth — та самая глубина обхода из восьмого урока, по которой colly отсекает рекурсию; в журнале она подсказывает, откуда пришёл запрос. У объекта есть и номер: r.ID уникален в пределах коллектора, и по нему удобно склеивать запрос с ответом в общем журнале. Самое ценное — r.Headers: это указатель на реальные заголовки запроса, и всё, что здесь выставить методом Set, отправится на сервер как есть. Именно в OnRequest живут User-Agent, Referer и Accept-Language — механике и границам этих манипуляций посвящён отдельный урок про [заголовки и User-Agent](/courses/colly/lessons/14-zagolovki-useragent/). И последнее: OnRequest — последняя точка, где запрос ещё можно отменить, об этом ниже.

### Журнал запроса и ответа

```go
c.OnRequest(func(r *colly.Request) {
        log.Println("->", r.Method, r.URL, "глубина", r.Depth)
})

c.OnResponse(func(r *colly.Response) {
        log.Printf("<- %d %s, %d байт", r.StatusCode, r.Request.URL, len(r.Body))
})
```

OnRequest печатает метод и адрес до отправки, OnResponse — статус, итоговый адрес и размер тела после загрузки. r.Request.URL в OnResponse — адрес последнего запроса цепочки: после редиректа он отличается от стартового. Две стрелки в логе превращают невидимый конвейер в читаемый диалог — любая пауза, пустой вывод или подозрительный код теперь имеют адрес и причину.

### colly onresponse: тело уже скачано

OnResponse с сигнатурой func(r *colly.Response) вызывается, когда тело ответа целиком лежит в памяти. Что внутри: r.StatusCode — число, а не строка; r.Body — срез байт, тот самый HTML или JSON; r.Headers — заголовки ответа, из них r.Headers.Get("Content-Type") достаёт тип содержимого; r.Request — связанный объект запроса с адресом и глубиной. Обратите внимание: отдельного поля ContentType у Response нет — тип берётся из заголовков, и запомнить это стоит заранее, чтобы не искать несуществующее поле по советам старых статей. Тело — сырые байты, и это удобно: string(r.Body) позволяет поискать подстроку по сырой разметке ещё до селекторов, len(r.Body) — посчитать размер, а сравнение размеров между прогонами — заметить, что сайт начал отдавать заглушку вместо каталога. На этот колбэк приходят и не-HTML ответы: JSON-эндпоинты, файлы, заглушки — всё, что вернул сервер с успешным кодом. Тело можно отправить в свой парсер или сохранить на диск методом r.Save("page.html") — быстрый снапшот страницы для отладки селекторов без браузера и curl. Имя файла, если сервер передал его в Content-Disposition, подскажет r.FileName().

### Порядок вызовов: конвейер от Visit до OnScraped

Колбэки colly срабатывают в строгом порядке, и он одинаков для каждой страницы, сколько бы обработчиков вы ни зарегистрировали. Знать конвейер наизусть полезно по двум причинам: сразу видно, на каком этапе пропала строка лога, и ясно, где ставить проверку — раньше значит дешевле. Выглядит конвейер так: OnRequest, затем сеть, затем OnResponseHeaders, OnResponse, все OnHTML по порядку регистрации — и финальный OnScraped, который замыкает страницу целиком. Разберём шаги по одному.

1. OnRequest — запрос собран и скоро уйдёт; здесь живут заголовки, журнал и Abort.
2. Сеть — HTTP-клиент отправляет запрос; редиректы транспорт догоняет сам, colly видит финальную страницу.
3. OnResponseHeaders — получены статус и заголовки ответа, тело ещё не читалось.
4. OnResponse — тело скачано целиком и лежит в r.Body как срез байт.
5. OnHTML (и OnXML) — DOM разобран, селекторы отрабатывают в порядке регистрации обработчиков.
6. OnScraped — страница обработана целиком; удобная финальная точка для сводки по ней.

### OnResponseHeaders: решение до скачивания тела

Между заголовками и телом есть отдельный колбэк — OnResponseHeaders(func(r *colly.Response)). Тело в этот момент ещё не скачивалось: доступен r.StatusCode, доступны r.Headers, а r.Body пуст. Это идеальное место для решений, которым хватает заголовков. Видали Content-Type text/html — отдаём страницу парсеру; увидели application/json — это эндпоинт, и данные достаются без разбора DOM; заметили image/ — можно сразу отказаться от ответа. То же с размером из заголовков: огромный файл виден в очереди до того, как съест трафик. Тонкость, о которой молчат туториалы: OnResponseHeaders срабатывает у всех дошедших ответов, включая 403 и 404 — заголовки-то уже приехали, и вы успеете залогировать код раньше, чем colly решит судьбу ответа. Abort работает и здесь: вызванный в этом колбэке, он обрывает докачку тела, а в OnError приходит ошибка ErrAbortedAfterHeaders — так экономят трафик на тяжёлых ответах, которые заведомо не нужны. Разница с OnResponse только в моменте, но момент дорогой: после заголовков тело ещё можно не качать, после — уже поздно.

### Фильтр по типу содержимого

```go
c.OnResponseHeaders(func(r *colly.Response) {
        ct := r.Headers.Get("Content-Type")

        if strings.HasPrefix(ct, "image/") {
                r.Request.Abort() // тело картинки не докачиваем
                return
        }

        log.Println("тип:", ct, "статус:", r.StatusCode)
})
```

Content-Type приезжает в заголовках, поэтому решение принимается до чтения тела. Отмена делается через r.Request.Abort(): проверка в колбэке вернёт false, библиотека закроет тело не читая, а в OnError прилетит ErrAbortedAfterHeaders — это штатный способ отказаться от ответа, а не поломка, и пугаться такой строки в журнале не нужно.

### Куда уходят 4xx, 5xx и редиректы

Успешные ответы проходят конвейер целиком и заканчиваются в OnHTML и OnScraped. Всё остальное colly проводит по другой ветке: ответы с кодами ошибок — 403, 404, 500 — попадают в OnError, и OnResponse с OnHTML их не видят; подробно эту ветку и повтор запросов мы разбирали в уроке про [OnError и повторы](/courses/colly/lessons/16-oshibki-retry/). Разделение логичное: страница с 404 обычно не несёт данных, и гонять её в DOM — только тратить память, а вот журнал ошибок обязан знать о каждом провале. Редиректы — отдельная история: перенаправления с заголовком Location HTTP-транспорт проходит сам, colly даже не узнаёт о промежуточных адресах — в OnResponse прилетает уже финальная страница с кодом 200, а r.Request.URL показывает её итоговый адрес. Поэтому «проверить статус 301 в OnResponse» не получится: до вас дойдёт только конец цепочки, и это к лучшему — бегать за редиректами вручную не нужно. Тело ответа с ошибкой по умолчанию не разбирается вовсе — опция ParseHTTPErrorResponse меняет это поведение, если страницы ошибок несут данные.

> **OnResponse не ловит 404.** Самая частая потеря новичка: обработку ошибок пишут в OnResponse, потому что «ответ же пришёл». Ответ с кодом 404 или 500 для colly — ошибка: OnResponse не вызовется, OnHTML не увидит ни одного элемента, и всё замолчит, если OnError не зарегистрирован. Проверка if r.StatusCode >= 400 внутри OnResponse не сработает никогда — без ParseHTTPErrorResponse до неё дело просто не доходит.

### Abort: отсекаем мусор до отправки

В восьмом уроке мы фильтровали [ссылки до Visit](/courses/colly/lessons/08-navigaciya-ssylki/) — это санитария очереди. Но запрос может оказаться мусорным и позже: пагинация ведёт на страницы-приложения в PDF, каталог раздаёт картинки прямыми ссылками, а вы по ошибке собрали их в очередь. Для таких случаев в OnRequest есть рубильник r.Abort(): запрос, помеченный aborted, в сеть не уходит вообще. Причём это не ошибка — OnError не вызовется, Visit завершится тихо, очередь продолжит работу со следующим адресом. Связка получается двухуровневой: фильтр ссылок держит очередь чистой на этапе планирования, Abort — последняя проверка на этапе исполнения, когда URL уже поставлен в очередь и до сети остался один шаг. Разницу видно в журнале: провал попадает в OnError, а отмена — тишина, поэтому отмены стоит логировать прямо в OnRequest, печатая адрес перед Abort-ом. Мой рабочий минимум: Abort на расширения статики и архивные форматы, которые точно не несут данных, — трафик экономится заметнее всего именно здесь, потому что отменённый запрос не отправляется вовсе.

### Отмена статики в OnRequest

```go
c.OnRequest(func(r *colly.Request) {
        for _, ext := range []string{".jpg", ".png", ".gif", ".pdf", ".zip"} {
                if strings.HasSuffix(r.URL.Path, ext) {
                        r.Abort()
                        return
                }
        }

        log.Println("грузим:", r.URL)
})
```

Путь запроса достаётся из r.URL.Path — это разобранный *url.URL, без схемы и хоста. Суффикс проверяется по списку расширений, и совпавший запрос помечается Abort-ом прямо в колбэке. Отменённые адреса не попадают ни в OnResponse, ни в OnError — в журнале их видно только по отсутствию парной стрелки: request ушёл, response не пришёл.

### Кто что видит на каждом этапе

| Этап | Что доступно | Типичное применение |
| --- | --- | --- |
| OnRequest | r.URL, r.Headers, r.Depth, r.Abort | журнал, заголовки запроса, отсечение статики |
| OnResponseHeaders | StatusCode и заголовки ответа; Body ещё пуст | фильтр по Content-Type, ранний лог |
| OnResponse | StatusCode, Body, Headers, Request, Save | разбор JSON, снапшоты, подсчёт байт |
| OnHTML | DOM-элементы, e.Request | извлечение данных селекторами |
| OnError | Response без тела, err | журнал провалов, очередь повторов |
| OnScraped | тот же Response | сводка по обработанной странице |

### Проверка: судьба 404

**Вопрос:** Сайт ответил 404. Какие колбэки вызовутся и в каком порядке?

- ✅ OnResponseHeaders, затем OnError; OnResponse и OnHTML молчат
- OnResponse, затем OnError: ответ ведь пришёл
- OnHTML получит страницу ошибки и попробует применить селекторы
- Ничего: colly молча пропустит запрос без вызовов

Заголовки ответа доставлены, поэтому OnResponseHeaders успевает отработать даже у 404. Всё, что требует тела, отменяется: OnResponse и OnHTML видят только успешные ответы, а ошибку встречает OnError. Тело ошибки достаётся опцией ParseHTTPErrorResponse, если оно действительно нужно.

### Проверка: Abort

**Вопрос:** В OnRequest для всех адресов с суффиксом .pdf вызван r.Abort(). Что увидит программа?

- В OnError придёт ошибка aborted: запрос считается проваленным
- ✅ Запрос не уйдёт в сеть: ни OnResponse, ни OnHTML, ни OnError не вызовутся
- OnResponse вызовется с пустым Body
- colly поставит URL в очередь на повторную загрузку

Abort ломает конвейер сразу после OnRequest: запрос не отправляется, обработка завершается тихо, Visit не возвращает ошибку. Для colly это решение не грузить адрес, а не авария — поэтому и OnError остаётся молчаливым.

### Что напечатает программа при 500?

```go
c.OnRequest(func(r *colly.Request) {
        fmt.Println("->", r.URL)
})

c.OnResponse(func(r *colly.Response) {
        fmt.Println("<-", r.StatusCode)
})

c.OnError(func(r *colly.Response, err error) {
        fmt.Println("!", r.StatusCode, err)
})

c.Visit("http://books.toscrape.com/broken")
```

**Что выведет программа?**

- -> URL, затем <- 500
- ✅ -> URL, затем ! 500 Internal Server Error
- Только <- 500
- -> URL, <- 500, затем ! 500

OnRequest отрабатывает до отправки всегда — первая стрелка напечатается обязательно. Ответ с кодом 500 colly встречает как ошибку: OnResponse не вызывается, OnError получает код и текст статуса, собранный из http.StatusText. Ту же ошибку вернёт и сам Visit.

### Соберите пару запрос-ответ

Восстановите регистрацию колбэков, отмену статики и сохранение страницы.

```go
c.___(func(r *colly.Request) {
        if strings.HasSuffix(r.URL.Path, ".zip") {
                r.___()
        }
})

c.___(func(r *colly.Response) {
        fmt.Println(r.___, len(r.Body))
        r.___("catalog/page.html")
})
```

Подсказка: До сети — OnRequest с Abort-ом; после загрузки — OnResponse: код достаётся из StatusCode, файл пишет Save.

Правильные ответы в пропусках: `OnRequest`, `Abort`, `OnResponse`, `StatusCode`, `Save`

### Соберите конвейер по строкам

Расставьте строки в порядке конвейера: от запроса до ответа, ошибки — рядом с ответом.

Правильный порядок строк:

1. `c := colly.NewCollector()`
2. `c.OnRequest(func(r *colly.Request) { fmt.Println("->", r.URL) })`
3. `c.OnResponseHeaders(func(r *colly.Response) { fmt.Println("headers", r.StatusCode) })`
4. `c.OnResponse(func(r *colly.Response) { fmt.Println("<-", len(r.Body)) })`
5. `c.OnError(func(r *colly.Response, err error) { fmt.Println("!", err) })`
6. `c.Visit("http://books.toscrape.com/")`

### Карточки: запрос и ответ

- **OnRequest** — Колбэк перед отправкой запроса: r.URL, r.Headers, r.Depth. Здесь выставляют заголовки, ведут журнал и вызывают Abort.
- **OnResponse** — Колбэк после загрузки тела: StatusCode, Body []byte, Headers, Request. Вызывается только у успешных ответов — 4xx и 5xx уходят в OnError.
- **OnResponseHeaders** — Между заголовками и телом: статус и заголовки видны, Body ещё пуст. Срабатывает у всех дошедших ответов, включая 403 и 404.
- **r.Abort()** — Отмена запроса. В OnRequest запрос не уйдёт в сеть и ошибкой не считается. В OnResponseHeaders не докачается тело, а в OnError придёт ErrAbortedAfterHeaders.
- **r.Save(fileName)** — Пишет r.Body в файл. Удобно в OnResponse для снапшотов HTML и сохранения скачанных документов.
- **OnScraped** — Финальный колбэк страницы: вызывается после всех OnHTML и OnXML. Единая точка «страница обработана» — для сводок и статистики.

### Что дальше

Конвейер виден, каждый запрос подписан — остался вопрос темпа: обходчик ставит запросы в очередь так быстро, как успевает сеть, и без ограничителей это подозрительно похоже на DDoS. Успокоим его в следующем уроке про LimitRule: там появятся Delay, RandomDelay и лимит одновременных запросов на домен. Примеры этого урока, как и весь сетевой код курса, запускаются локальным Go — в браузерной песочнице сети нет. Соберите журнал запроса и ответа, прогоните его по books.toscrape.com и полистайте терминал: каталог из пары страниц прочитывается как диалог, и каждая стрелка в нём — вызов одного из колбэков этого урока. Полный маршрут по курсу — на странице [коли-курса](/courses/colly/).

## Коротко: что запомнить

- OnRequest вызывается перед отправкой: отсюда видны r.URL, r.Method и r.Depth, здесь выставляются заголовки и работает Abort.
- Порядок конвейера: OnRequest → сеть → OnResponseHeaders → OnResponse → OnHTML → OnScraped.
- OnResponseHeaders видит статус и Content-Type до чтения тела — место для решений, которым HTML не нужен.
- OnResponse получает StatusCode и r.Body []byte, но только у успешных ответов; 4xx и 5xx уходят в OnError.
- Abort в OnRequest отменяет запрос до сети: трафик не тратится, OnError не вызывается, очередь идёт дальше.
- Редиректы http-транспорт проходит сам: в OnResponse приходит финальная страница, r.Request.URL показывает её адрес.
- r.Save(fileName) пишет тело на диск — быстрый снапшот страницы для отладки селекторов.

## Домашнее задание

Добавьте в обходчик из восьмого урока пару OnRequest и OnResponse с журналом: адрес, метод, статус, размер тела. Отсеките Abort-ом картинки и архивы в OnRequest, а в OnResponseHeaders — ответы с типом image/. Запустите локально на books.toscrape.com и посчитайте по логу, сколько запросов сэкономил Abort.

## Вопросы и ответы по уроку

### Почему OnResponse не срабатывает на странице с кодом 404?

Ответ с ошибкой colly проводит по ветке OnError: OnResponse и OnHTML видят только успешные ответы. Заголовки при этом доходят — OnResponseHeaders успевает отработать. Тело страницы ошибки можно получить опцией ParseHTTPErrorResponse, но селекторам там обычно ловить нечего.

### Чем OnResponseHeaders отличается от OnResponse?

Моментом вызова и наполнением: OnResponseHeaders срабатывает сразу после заголовков, когда Body ещё пуст — видно только StatusCode и Headers. OnResponse вызывается после скачивания тела. Если решение зависит только от заголовков, не ждите тело — фильтруйте или отменяйте раньше.

### Можно ли в OnRequest узнать, куда именно уйдёт запрос?

Да: r.URL — разобранный *url.URL с хостом, путём и query-параметрами, r.Method — метод, r.Depth — глубина обхода. Заголовки меняются прямо через r.Headers.Set. Объект тот же, что и e.Request в OnHTML, поэтому AbsoluteURL и Visit доступны и отсюда.
