---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Obsługa błędów HTTP w sposób mniej uciążliwy + RFC7807
description: Czy obsługa błędów w Go API jest uciążliwa? Spróbuj rozwiązania opartego na RFC7807, które pozwala na prostszą i bardziej przejrzystą obsługę.
language: pl
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Przegląd

Podczas tworzenia API HTTP w języku Go najbardziej uciążliwa jest obsługa błędów. Typowym przykładem jest poniższy kod.

```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
    }
    // ...
}
```

Jeżeli API jest nieliczne, pisanie w ten sposób nie sprawia szczególnych trudności. Wraz ze wzrostem liczby API i komplikacją logiki wewnętrznej zaczynają jednak przeszkadzać trzy kwestie:

1. Zwracanie odpowiedniego kodu błędu
2. Duża liczba zapisów logów wyników
3. Przesyłanie czytelnego komunikatu o błędzie

## Część główna

### Zwracanie odpowiedniego kodu błędu

Punkt 1, czyli zwracanie odpowiedniego kodu błędu, jest oczywiście moją osobistą uciążliwością. Doświadczony programista zawsze znajdzie właściwy kod i wstawi go poprawnie, jednak ja, podobnie jak mniej doświadczeni programiści, mogę mieć trudności z konsekwentnym stosowaniem odpowiednich kodów błędów, gdy logika staje się złożona, a liczba wywołań rośnie. Istnieje wiele podejść do tego problemu, a najbardziej typowym jest wcześniejsze zaprojektowanie przepływu logiki API i napisanie kodu, który zwraca właściwy błąd. ~~Proszę tak zrobić~~

Nie wydaje się jednak, aby było to optymalne rozwiązanie dla programisty-człowieka, który korzysta z pomocy IDE (lub Language Server). Ponadto, ponieważ samo REST API maksymalnie wykorzystuje znaczenie zawarte w kodach błędów, można zaproponować inne podejście. Należy utworzyć nową implementację interfejsu błędu (`error`) o nazwie `HttpError`, która przechowuje `StatusCode` oraz `Message`. Następnie udostępnia się funkcje pomocnicze, takie jak poniższa.

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

Funkcja pomocnicza `BadRequest` zwróci `HttpError`, w którym `StatusCode` ma wartość 400, a `Message` zostaje ustawiony na przekazany argument. Oprócz tego można oczywiście wyszukiwać i dodawać funkcje pomocnicze, takie jak `NotImplement`, `ServiceUnavailable`, `Unauthorized` czy `PaymentRequired`, korzystając z autouzupełniania. Jest to szybsze niż każdorazowe sprawdzanie przygotowanego schematu projektu i bardziej niezawodne niż ręczne wpisywanie kodów liczbowych. ~~Mówicie, że wszystko znajduje się w stałych `http.StatusCode`? Ciii~~

### Duża liczba zapisów logów wyników

W przypadku wystąpienia błędu oczywiście zapisuje się log. Przy rejestrowaniu wywołania API oraz tego, czy żądanie zakończyło się powodzeniem, czy niepowodzeniem, zapisywanie logów we wszystkich przewidywanych punktach zakończenia, od samego początku, znacząco zwiększa liczbę pisanego kodu. Rozwiązaniem jest owinięcie samego handlera, dzięki czemu logowanie można zarządzać centralnie.

Poniżej znajduje się przykład owijania routera `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)
		}
	})
}
```

Struktura `Router` przechowuje wewnętrznie `chi.Router`, dzięki czemu można z niej korzystać z pełnym zestawem funkcji `chi.Router`. W metodzie `Get` sprawdzane jest, czy zwrócony został obiekt `HttpError`, zwracany przez funkcje pomocnicze zaproponowane wyżej, i odpowiednio na jego podstawie wysyłana jest odpowiedź. Jeśli zwrócony błąd jest typu `error`, jest on jednolicie przekazywany do funkcji zwrotnej obsługi błędów. Tę funkcję zwrotną otrzymuje się przez konstruktor.

Poniżej znajduje się kod napisany z wykorzystaniem tego pakietu.

```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())
}
```

Jak to wygląda? Wystarczy utworzyć jedynie funkcje pomocnicze zwracające `HttpError`, aby na poziomie nadrzędnym odsyłać odpowiedź z właściwym kodem i komunikatem błędu, a przez rejestrację funkcji zwrotnej umożliwić każdej usłudze zapisywanie odpowiednich logów. W razie potrzeby można to dodatkowo rozszerzyć, na przykład przez użycie `RequestID`, co pozwoli na szczegółowe logowanie.

### Przesyłanie czytelnego komunikatu o błędzie

Dokumentem określającym standard w tym zakresie jest RFC7807. RFC7807 definiuje i wykorzystuje głównie następujące elementy:

- `type`: URI identyfikujący typ błędu. Zazwyczaj jest to dokument opisujący dany błąd.
- `title`: jednozdaniowy opis przedstawiający rodzaj błędu.
- `status`: taki sam jak kod statusu HTTP.
- `detail`: szczegółowy opis danego błędu, czytelny dla człowieka.
- `instance`: URI, w którym wystąpił błąd. Na przykład jeśli błąd wystąpił przy `GET /user/info`, wartością będzie `/user/info`.
- `extensions`: dodatkowe elementy opisujące błąd, tworzone jako obiekt JSON.
  - Na przykład w przypadku `BadRequest` mogą zostać uwzględnione dane wejściowe użytkownika.
  - Alternatywnie w przypadku `TooManyRequest` można uwzględnić czas ostatniego żądania.

Aby ułatwić korzystanie z tego standardu, w pakiecie `httperror`, w tym samym miejscu co `HttpError`, tworzy się nowy plik, definiuje się strukturę `RFC7807Error` i umożliwia jej tworzenie za pomocą wzorca łańcuchowego wywołania metod.

```go
func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
	return &RFC7807Error{
		Type:   "about:blank", // Domyślny typ zgodnie z 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 { ... }
```

Wartość `"about:blank"` pola `Type` jest wartością domyślną. Oznacza ona brak strony. Poniżej znajduje się przykład utworzenia błędu dla nieprawidłowego żądania.

```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")
```

Dzięki prostemu łańcuchowemu wywoływaniu metod można wygenerować ustrukturyzowany komunikat o błędzie dla użytkownika. Aby ponadto korzystać z wcześniej przedstawionego scentralizowanego routera, można udostępnić następującą metodę.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Jeśli serializacja się nie powiedzie, użyj jedynie pola detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Po zastosowaniu tej metody przykład powyżej przyjmuje następującą postać.

```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())
}
```

## Zakończenie

Dzięki obsłudze błędów z wykorzystaniem scentralizowanego routera zmniejsza się obciążenie związane z każdorazowym sprawdzaniem kodów błędów i pisaniem odpowiednich komunikatów. Ponadto, wykorzystując RFC7807 do dostarczania ustrukturyzowanych komunikatów o błędach, można pomóc klientom w zrozumieniu błędu i jego obsłudze. Dzięki tym metodom obsługa błędów w HTTP API napisanym w języku Go może stać się prostsza i bardziej spójna.

Kod z tego artykułu można znaleźć w repozytorium [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
