---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Ako spracovať HTTP chyby menej neprehľadne a pohodlnejšie + RFC7807
description: Máte problémy so spracovaním chýb v Go API? Vyskúšajte RFC 7807, ktorý umožňuje spracovanie chýb jednoduchšie a jasnejšie.
language: sk
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Prehľad

Pri vytváraní HTTP API v jazyku Go je najotravnejšia práca spracovanie chýb. Typicky vyzerá 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
    }
    // ...
}
```

Ak je API len niekoľko, písanie takýmto spôsobom nebude nijako nepohodlné. Keď sa však počet API zvyšuje a vnútorná logika sa stáva zložitejšou, tri veci začnú byť rušivé:

1. Vracanie vhodného chybového kódu
2. Veľký počet zápisov do logu
3. Odosielanie jasnej chybovej správy

## Hlavná časť

### Vracanie vhodného chybového kódu

Samozrejme, bod 1, vracanie vhodného chybového kódu, je mojou osobnou sťažnosťou. Skúsený vývojár nájde vhodný kód a vždy ho správne vloží, ale ja aj ešte neskúsení vývojári môžeme mať ťažkosti s pravidelným používaním vhodného chybového kódu, keď logika končí zložitejšie a počet volaní narastá. Existuje na to viacero spôsobov, najtypickejší je vopred navrhnúť tok logiky API a potom napísať kód, ktorý vracia vhodnú chybu. ~~Urobte tak~~

Toto však nevyzerá ako optimálny prístup pre ľudského vývojára, ktorý používa IDE (alebo Language Server). Keďže samotné REST API maximálne využíva význam obsiahnutý v chybových kódoch, môžeme navrhnúť iný spôsob. Vytvoríme novú implementáciu rozhrania chyby (`error`) s názvom `HttpError`, ktorá uchováva `StatusCode` a `Message`. Potom poskytneme pomocné funkcie ako nasledujúca.

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

Funkcia `BadRequest` vráti `HttpError`, ktorého `StatusCode` nastaví na 400 a `Message` na hodnotu prijatú ako argument. Okrem toho môžeme samozrejme pomocou automatického dopĺňania vyhľadávať a pridávať ďalšie pomocné funkcie, ako sú `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired` a podobne. To je rýchlejšie než kontrolovanie pripraveného návrhu pri každom použití a stabilnejšie než zadávanie chybového kódu ako čísla pri každom volaní. ~~Konštanty `http.StatusCode` obsahujú všetko, hovoríte? Ticho~~

### Veľký počet zápisov do logu

Pri výskyte chyby sa samozrejme zapisuje log. Pri zapisovaní logu o tom, či bolo volanie API úspešné alebo neúspešné, znamená zapisovať log na každom očakávanom ukončovacom bode od začiatku veľa kódu. Ak celý handler obalíme, môžeme logovanie spravovať centrálne.

Nasleduje príklad obaľovania 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)
		}
	})
}
```

Štruktúra routera má vo vnútri `chi.Router`, takže je nakonfigurovaná tak, aby sa priamo používali funkcie `chi.Router`. Pri pohľade na metódu `Get` sa kontroluje, či bola vrátená štruktúra `HttpError`, ktorú vracia pomocná funkcia navrhnutá vyššie, a podľa toho sa vhodne odpovie. Ak ide o bežnú `error`, jednotne sa odovzdá chybovej callback funkcii. Tento callback sa prijíma cez konštruktor.

Nasleduje kód napísaný s použitím tohto balíka.

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

Čo si o tom myslíte? Ak len vytvoríte `HttpError` pomocou pomocných funkcií a vrátite ho, vyššia úroveň môže odpovedať vhodným chybovým kódom a správou. Zároveň registrovaním callbacku môže každá implementovaná služba zapisovať vhodný log. Ak je to potrebné, môžete to ďalej rozšíriť a použiť napríklad `RequestID` na podrobné logovanie.

### Odosielanie jasnej chybovej správy

Na tento účel existuje dokument RFC7807. RFC7807 definuje a používa hlavne nasledujúce prvky:

- `type`: URI identifikujúci typ chyby. Zvyčajne je to dokument, ktorý chybu popisuje.
- `title`: Jednoriadkový popis toho, o akú chybu ide.
- `status`: Rovnaký ako HTTP Status Code.
- `detail`: Podrobný popis chyby čitateľný pre človeka.
- `instance`: URI, na ktorom chyba nastala. Napríklad ak chyba nastala pri `GET /user/info`, hodnotou bude `/user/info`.
- `extensions`: Doplnkový prvok na opis chyby, tvorený JSON objektom.
  - Napríklad pri `BadRequest` môže obsahovať vstup používateľa.
  - Alebo pri `TooManyRequest` môže obsahovať čas najnovšej požiadavky.

Na jednoduché používanie vytvoríme nový súbor v balíku `httperror`, rovnako ako pri `HttpError`, vytvoríme štruktúru `RFC7807Error` a umožníme jej vytváranie vzorom reťazenia metód.

```go
func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
	return &RFC7807Error{
		Type:   "about:blank", // Predvolený typ podľa 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"` v `Type` je predvolená. Označuje stránku, ktorá neexistuje. Nižšie je príklad vytvorenia chybu pri nesprávnej požiadavke.

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

Jednoduchým reťazením metód môžete vytvoriť štruktúrovanú chybovú správu pre používateľa. Aby sa dal použiť centralizovaný router vytvorený vyššie, môžeme podporiť aj nasledujúcu metódu.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Ak marshaling zlyhá, použije sa len detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Použitím tejto metódy a úpravou predchádzajúceho príkladu dostaneme:

```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áver

Používaním centralizovaného routera na spracovanie chýb sa znižuje záťaž spojená s kontrolou chybových kódov a písaním vhodných chybových správ. Použitím RFC7807 na poskytovanie štruktúrovaných chybových správ môžeme navyše klientovi pomôcť chybu pochopiť a spracovať. Týmito prístupmi možno spracovanie chýb v HTTP API napísanom v jazyku Go urobiť jednoduchším a konzistentnejším.

Zdrojový kód tohto článku môžete nájsť v repozitári [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
