---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: По-удобна обработка на HTTP грешки + RFC7807
description: Излизате ли уморени от обработката на грешките в Go API? Опитайте с RFC7807 за по-лесна и ясна обработка.
language: bg
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Общ преглед

Когато създавате HTTP API на езика Go, най-досадната част е обработката на грешки. Типичен пример е следният код:

```go
func(w http.ResponseWriter, r *http.Request) {
    err := doSomething()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        log.Printf("error occurred: %v", err)
        return
    }
    // ...
}
```

Ако API-тата са малко, писането по този начин не създава особени неудобства. Въпреки това, с увеличаването на броя на API-тата и усложняването на вътрешната логика, три неща започват да ви притесняват:

1. Връщане на подходящ код за грешка
2. Голям брой записи в журнала
3. Изпращане на ясно съобщение за грешка

## Основна част

### Връщане на подходящ код за грешка

Първата точка, връщането на подходящ код за грешка, е моето лично неудобство. Опитен разработчик би намирал подходящия код и би го вписвал правилно всеки път, но както и аз, така и по-неопитните разработчици може да срещнат затруднения да използват последователно подходящи кодове за грешка, когато логиката стане сложна и броят на извикванията нарасне. По този въпрос съществуват различни подходи, като най-типичният е предварително проектиране на логическия поток на API и след това писане на кода така, че да връща подходящите грешки. ~~Направете така~~

Този подход обаче не изглежда оптимален за човешки разработчик, който разчита на помощта на IDE (или Language Server). Освен това, тъй като самият REST API максимално използва значението, заложено в кодовете за грешка, може да се предложи друг подход. Създава се нов тип грешка, реализиращ интерфейса `error`, наречен `HttpError`, който съхранява `StatusCode` и `Message`. След това се предоставят помощни функции като следната:

```go
err := httperror.BadRequest("wrong format")
```

Помощната функция `BadRequest` ще върне `HttpError`, в който `StatusCode` е зададен на 400, а `Message` е стойността, получена като аргумент. Освен нея, разбира se, могат да се създадат и помощни функции като `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired` и други, които могат да се разглеждат и добавят чрез функцията за автоматично допълване. Това е по-бързо от проверка на предварително подготвен проектен документ при всяка грешка и по-надеждно от въвеждане на числови кодове за грешка всеки път. ~~Всичко това е в константите `http.StatusCode`? Тссс~~

### Голям брой записи в журнала

При възникване на грешка, естествено, се записва съобщение в журнала. Когато се записва информация за извикването на API и за това дали заявката е успешна или неуспешна, записването на журнал във всяка очаквана точка на завършване на функцията увеличава значително обема на кода. Чрез обвиване на самия обработчик (handler) можем да управляваме това централно.

Следва пример за обвиване на маршрутизатора `chi`.

```go
package chiwrap

import (
	"errors"
	"net/http"

	"github.com/go-chi/chi/v5"

	"github.com/gosuda/httpwrap/httperror"
)

type Router struct {
	router      chi.Router
	errCallback func(err error)
}

func NewRouter(errCallback func(err error)) *Router {
	if errCallback == nil {
		errCallback = func(err error) {}
	}
	return &Router{
		router:      chi.NewRouter(),
		errCallback: errCallback,
	}
}

type HandlerFunc func(writer http.ResponseWriter, request *http.Request) error

func (r *Router) Get(pattern string, handler HandlerFunc) {
	r.router.Get(pattern, func(writer http.ResponseWriter, request *http.Request) {
		if err := handler(writer, request); err != nil {
			he := &httperror.HttpError{}
			switch errors.As(err, &he) {
			case true:
				http.Error(writer, he.Message, he.Code)
			case false:
				http.Error(writer, err.Error(), http.StatusInternalServerError)
			}
			r.errCallback(err)
		}
	})
}
```

Структурата `Router` съдържа `chi.Router` като вътрешен компонент, поради което функционалността на `chi.Router` може да се използва директно. В метода `Get` се проверява дали върнатата грешка е структурата `HttpError`, която връщат помощните функции, предложени по-горе, и при необходимост се връща подходящ отговор. Ако грешката е от общ тип `error`, тя се предава на функцията за обратно извикване (callback). Тази функция се получава през конструктора.

Следва код, написан с помощта на този пакет.

```go
package main

import (
	"bytes"
	"context"
	"errors"
	"io"
	"log"
	"net/http"

	"github.com/gosuda/httpwrap/httperror"
	"github.com/gosuda/httpwrap/wrapper/chiwrap"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

	r := chiwrap.NewRouter(func(err error) {
		log.Printf("Router log test: Error occured: %v", err)
	})
	r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
		name := request.URL.Query().Get("name")
		if name == "" {
			return httperror.BadRequest("name is required")
		}

		writer.Write([]byte("Hello " + name))
		return nil
	})

	svr := http.Server{
		Addr:    ":8080",
		Handler: r,
	}
	go func() {
		if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("Failed to start server: %v", err)
		}
	}()

    <-ctx.Done()
    svr.Shutdown(context.Background())
}
```

Какво мислите? Ако създадем като помощна функция само `HttpError` и го връщаме, то във външния обхват може да се върне подходящ отговор с подходящ код за грешка и съобщение, а в зависимост от услугата може да се регистрира обратно извикване, което записва подходящ журнал. Ако е необходимо, това може да се разшири, например чрез използване на `RequestID`, за по-подробно журналиране.

### Изпращане на ясно съобщение за грешка

За тази цел съществува документът RFC7807. RFC7807 дефинира и използва главно следните елементи:

- `type`: URI, което идентифицира типа на грешката. Обикновено е документ, който описва грешката.
- `title`: Кратко описание в един ред на това каква е грешката.
- `status`: Съвпада с HTTP кода на състоянието.
- `detail`: Подробно описание на грешката, четимо от човек.
- `instance`: URI, при което е възникнала грешката. Например, ако грешката е възникнала при `GET /user/info`, стойността ще бъде `/user/info`.
- `extensions`: Допълнителни елементи за описание на грешката, структурирани като JSON обект.
  - Например, при `BadRequest` може да се включи въведеното от потребителя.
  - Или при `TooManyRequest` може да се включи времето на най-последната заявка.

За да се използва лесно, в пакета `httperror`, в същата директория като `HttpError`, се създава нов файл, в който се дефинира структурата `RFC7807Error`, а конструирането се позволява чрез верижно извикване на методи (method chaining).

```go
func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
	return &RFC7807Error{
		Type:   "about:blank", // Стойност по подразбиране според RFC7807
		Title:  title,
		Status: status,
		Detail: detail,
	}
}

func BadRequestProblem(detail string, title ...string) *RFC7807Error {
	t := "Bad Request"
	if len(title) > 0 && title[0] != "" {
		t = title[0]
	}
	return NewRFC7807Error(http.StatusBadRequest, t, detail)
}

func (p *RFC7807Error) WithType(typeURI string) *RFC7807Error { ... }
func (p *RFC7807Error) WithInstance(instance string) *RFC7807Error { ... }
func (p *RFC7807Error) WithExtension(key string, value interface{}) *RFC7807Error { ... }
```

Стойността `"about:blank"` на `Type` е по подразбиране. Тя означава празна страница, т.е. страница, която не съществува. Следва пример за създаване на грешка при неправилна заявка.

```go
problem := httperror.BadRequestProblem("invalid user id format", "Bad User Input")

problem = problem.WithType("https://example.com/errors/validation")
         .WithInstance("/api/users/abc")
         .WithExtension("invalid_field", "user_id")
         .WithExtension("expected_format", "numeric")
```

С помощта на прости верижни извиквания на методи може да се създаде структурирано съобщение за грешка за потребителя. За да се използва централизираният маршрутизатор, описан по-горе, може да се поддържа и следният метод.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Ако сериализацията се провали, използваме само detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Като използваме този метод директно, можем да променим горния пример по следния начин.

```go
package main

import (
	"bytes"
	"context"
	"errors"
	"io"
	"log"
	"net/http"

	"github.com/gosuda/httpwrap/httperror"
	"github.com/gosuda/httpwrap/wrapper/chiwrap"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

	r := chiwrap.NewRouter(func(err error) {
		log.Printf("Router log test: Error occured: %v", err)
	})
	r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
		name := request.URL.Query().Get("name")
		if name == "" {
			return httperror.BadRequestProblem("name is required", "Bad User Input").
                WithType("https://example.com/errors/validation").
                WithInstance("/api/echo").
                WithExtension("invalid_field", "name").
                WithExtension("expected_format", "string").
                WithExtension("actual_value", name).
                ToHttpError()
		}

		writer.Write([]byte("Hello " + name))
		return nil
	})

	svr := http.Server{
		Addr:    ":8080",
		Handler: r,
	}
	go func() {
		if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("Failed to start server: %v", err)
		}
	}()

    <-ctx.Done()
    svr.Shutdown(context.Background())
}
```

## Заключение

Когато грешките се обработват чрез централизиран маршрутизатор, се намалява натоварването от проверката на кодовете за грешка при всяко извикване и от писането на подходящи съобщения за грешка. Освен това, използвайки RFC7807 за предоставяне на структурирани съобщения за грешка, можем да помогнем на клиента да разбере грешката и да я обработи. По този начин обработката на грешки в HTTP API, написани на Go, може да стане по-лесна и последователна.

Кодът от тази статия може да бъде намерен в хранилището [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
