---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Jak s menším úsilím zpracovávat chyby HTTP + RFC7807
description: Je zpracování chyb v Go API únavné? Vyzkoušejte RFC7807 a zpracovávejte chyby jednodušeji a přehledněji.
language: cs
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Přehled

Při vytváření HTTP API v jazyce Go je nejotravnějším úkolem zpracování chyb. Typicky vypadá kód takto:

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

Pokud je API jen několik, není na psaní kódu tímto způsobem nic zvlášť nepohodlného. Jak ale počet API roste a vnitřní logika se komplikuje, začnou vadit tři věci.

1. Vracení vhodných chybových kódů
2. Množství zápisů do logu
3. Zasílání jasných chybových zpráv

## Hlavní část

### Vracení vhodných chybových kódů

Bod 1, vracení vhodných chybových kódů, je sice spíše mou osobní stížností. Zkušený vývojář najde vhodný kód a pokaždé ho správně doplní. Já i ostatní méně zkušení vývojáři však můžeme mít potíže s pravidelným používáním vhodných chybových kódů, jakmile je logika složitější a počet volání roste. Existuje k tomu řada přístupů, nejtypičtějším je předem navrhnout tok logiky API a poté psát kód, který vrací vhodné chyby. ~~Udělejte tak~~

Tento přístup se však nejeví jako optimální pro lidského vývojáře, který využívá pomoc IDE (nebo Language Serveru). Jelikož REST API samo o sobě co nejvíce využívá význam obsažený v chybových kódech, lze navrhnout i jiný způsob. Vytvoříme novou implementaci rozhraní `error` nazvanou `HttpError`, která uchovává `StatusCode` a `Message`. Dále poskytneme pomocné funkce:

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

Pomocná funkce `BadRequest` vrátí `HttpError`, jehož `StatusCode` je nastaven na 400 a `Message` na předanou hodnotu. Kromě ní lze samozřejmě přidat i funkce `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired` atd., které lze vyhledávat a doplňovat pomocí automatického doplňování. Je to rychlejší než pokaždé kontrolovat připravený návrh a stabilnější než ručně zadávat číselné chybové kódy. ~~Tvrdíte, že vše je v konstantách `http.StatusCode`? Ššš~~

### Množství zápisů do logu

Při výskytu chyby se samozřejmě zapisuje log. Pokud by se při zaznamenávání volání API a toho, zda požadavek uspěl nebo selhal, zapisoval log na každém očekávaném místě ukončení od začátku, vzrostlo by množství kódu. Proto lze handler obalit a správu logování soustředit na jedno místo.

Následuje příklad obalení routeru `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 routeru obsahuje `chi.Router`, takže se používají přímo funkce `chi.Router`. V metodě `Get` se kontroluje, zda byl vrácen `HttpError` vytvořený výše navrženými pomocnými funkcemi, a podle toho se vhodně odpovídá. Pokud jde o `error`, předá se jednotně chybové callback funkci. Tento callback se předává přes konstruktor.

Následuje kód napsaný s využitím tohoto balíčku.

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

Co si o tom myslíte? Pokud pouze vytvoříte `HttpError` pomocnou funkcí a vrátíte ho, může nadřazený rozsah odpovědět vhodným chybovým kódem a zprávou, zatímco každá implementovaná služba může zaregistrovat callback, který zapíše vhodný log. Pokud je to potřeba, lze to dále rozšířit o `RequestID` apod. pro podrobnější logování.

### Zasílání jasných chybových zpráv

K tomu slouží dokument RFC7807. RFC7807 obvykle definuje následující prvky:

- `type`: URI identifikující typ chyby. Obvykle je to dokument popisující danou chybu.
- `title`: jednořádkový popis toho, o jakou chybu jde.
- `status`: shodný s HTTP stavovým kódem.
- `detail`: podrobný popis chyby čitelný pro člověka.
- `instance`: URI, na kterém chyba nastala. Například pokud chyba nastala při `GET /user/info`, hodnotou bude `/user/info`.
- `extensions`: doplňkové prvky pro popis chyby ve formě JSON objektu.
  - Například u `BadRequest` může obsahovat uživatelský vstup.
  - Nebo u `TooManyRequest` může obsahovat čas nejnovějšího požadavku.

Pro snadné použití vytvoříme v balíčku `httperror`, kde se nachází `HttpError`, nový soubor, definujeme strukturu `RFC7807Error` a umožníme její vytváření pomocí vzoru řetězení metod.

```go
func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
	return &RFC7807Error{
		Type:   "about:blank", // Výchozí typ podle 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 { ... }
```

Hodnota `"about:blank"` u `Type` je výchozí. Označuje stránku, která neexistuje. Níže je příklad vytvoření chyby pro neplatný požadavek.

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

Pomocí jednoduchého řetězení metod lze vytvořit strukturovanou chybovou zprávu pro uživatele. Abychom mohli využít výše vytvořený centralizovaný router, lze podporovat také následující metodu:

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Pokud marshalování selže, použije se pouze detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Použitím této metody upravíme předchozí příklad takto:

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

## Závěr

Pokud se chyby zpracovávají pomocí tohoto centralizovaného routeru, ulehčí se zátěž spojená s kontrolou chybových kódů a psaním vhodných chybových zpráv při každém volání. Využitím RFC7807 pro poskytování strukturovaných chybových zpráv navíc může klient chybu snáze pochopit a zpracovat. Tímto způsobem lze zpracování chyb v HTTP API napsaném v jazyce Go učinit jednodušším a konzistentnějším.

Kód z tohoto článku lze najít v repozitáři [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
