---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Egyszerűbb HTTP-hibakezelés + RFC7807
description: Unalmas-e a Go API hibakezelés? Próbálja ki az RFC7807 szabvány alkalmazását, amellyel a hibák egyszerűbben és egyértelműbben kezelhetők.
language: hu
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Áttekintés

Go nyelven HTTP API-t készítve a legbosszantóbb a hibakezelés. Tipikus példa az alábbi kód.

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

Ha az API-k száma kevés, ilyen módon írva sem lesz különösebb kellemetlenség. Ahogy azonban az API-k száma nő és a belső logika bonyolultabbá válik, három dolog kezd zavarni:

1. Megfelelő hibakód visszaadása
2. Túl sok naplóbejegyzés írása
3. Egyértelmű hibaüzenet küldése

## Főrész

### Megfelelő hibakód visszaadása

Természetesen az 1. pont, vagyis a megfelelő hibakód visszaadása, a saját személyes panaszom. A tapasztalt fejlesztők minden alkalommal megtalálják és helyesen beírják a megfelelő kódot, de én is, és a kezdő fejlesztők is nehezen tudják következetesen használni a megfelelő hibakódokat, ahogy a logika bonyolultabbá válik és az esetek száma nő. Erre többféle megoldás létezik, a legelterjedtebb az, hogy előre megtervezzük az API logikai folyamatát, majd úgy írjuk meg a kódot, hogy a megfelelő hibát adja vissza. ~~Tegyék ezt meg~~

Ez azonban nem tűnik optimális megoldásnak olyan emberi fejlesztő számára, aki az IDE-(vagy Language Server-)től kap segítséget. Továbbá, mivel maga a REST API a hibakódokban rejlő jelentést igyekszik a lehető legjobban kihasználni, egy másik módszert is javasolhatunk. Létrehozunk egy új, `error` interfészt megvalósító `HttpError` típust, amely tárolja a `StatusCode`-ot és a `Message`-et. Ezután az alábbihoz hasonló segédfüggvényeket biztosítunk.

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

A `BadRequest` segédfüggvény olyan `HttpError`-t ad vissza, amelynek `StatusCode` értéke 400, `Message` értéke pedig az átadott argumentum. Ezen kívül természetesen létrehozhatunk olyan segédfüggvényeket is, mint a `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired`, amelyeket automatikus kiegészítéssel kereshetünk és vehetünk fel. Ez gyorsabb, mint minden alkalommal az előre elkészített tervdokumentációt ellenőrizni, és megbízhatóbb, mint a hibakódot minden alkalommal számként beírni. ~~Azt mondják, hogy minden benne van a `http.StatusCode` konstansokban? Pszt~~

### Túl sok naplóbejegyzés írása

Hiba esetén természetesen naplózni kell. Ha az API meghívásakor és a kérés sikerességének vagy sikertelenségének naplózásakor a kezdetektől fogva minden várható befejezési pontra naplóbejegyzést írunk, az sok kódot eredményez. Ezt úgy oldhatjuk meg, hogy magát a handlert egy burkolóval vesszük körül, így a naplózás központilag kezelhetővé válik.

Az alábbi példa a `chi` router becsomagolását mutatja.

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

A router struct belsejében egy `chi.Router` található, így a `chi.Router` funkciói változatlanul használhatók. A `Get` metódusban látható, hogy ellenőrzi, hogy a fentebb javasolt segédfüggvény által visszaadott `HttpError` struct érkezett-e, és ennek megfelelően válaszol. Minden más `error` esetén egységesen az error callback függvénynek adja át. Ezt a callbacket a konstruktor kapja meg paraméterként.

Az alábbi kód a csomag felhasználásával készült.

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

Nos, mit gondolnak? Ha csak a `HttpError`-t készítjük el segédfüggvényként és adjuk vissza, a felsőbb szinten megfelelő hibakóddal és üzenettel válaszolhatunk, miközben a regisztrált callbacken keresztül minden szolgáltatás számára megfelelő naplózást biztosíthatunk. Szükség esetén bővíthetjük is, például a `RequestID` használatával részletes naplózás valósítható meg.

### Egyértelmű hibaüzenet küldése

Erre a célra az RFC7807 szolgál. Az RFC7807 főként az alábbi elemeket határozza meg:

- `type`: a hibatípust azonosító URI. Főként a hibát leíró dokumentum.
- `title`: rövid, egysoros leírás arról, hogy milyen hibáról van szó.
- `status`: megegyezik a HTTP állapotkóddal.
- `detail`: az adott hibáról emberek számára olvasható, részletes leírás.
- `instance`: az URI, ahol a hiba történt. Például ha a hiba a `GET /user/info` kérésnél történt, ennek értéke `/user/info` lesz.
- `extensions`: JSON objektum formájában megadott, a hibát magyarázó kiegészítő elem.
  - Például `BadRequest` esetén tartalmazhatja a felhasználó bemenetét.
  - Vagy `TooManyRequest` esetén tartalmazhatja a legutóbbi kérés időpontját is.

Ennek egyszerű használatához a `HttpError`-rel azonos helyen, a `httperror` csomagban létrehozunk egy új fájlt, definiáljuk benne az `RFC7807Error` struct-ot, és metódusláncolási mintával tesszük lehetővé a létrehozását.

```go
func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
	return &RFC7807Error{
		Type:   "about:blank", // Alapértelmezett típus az RFC7807 szerint
		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 { ... }
```

A `Type` `"about:blank"` értéke az alapértelmezett érték. Egy nem létező oldalt jelöl. Az alábbiakban egy hibás kérésre vonatkozó hibagenerálási példa látható.

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

Egyszerű metódusláncolással a felhasználó számára strukturált hibaüzenet hozható létre. Ezen kívül, hogy a korábban bemutatott központosított routert használhassuk, a következő metódust támogathatjuk:

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Ha a szerializálás sikertelen, csak a detail mezőt használjuk
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Ezt közvetlenül használva a fenti példa így módosítható:

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

## Következtetés

A központosított router segítségével történő hibakezelés révén csökkenthető az a teher, hogy minden alkalommal ellenőriznünk kell a hibakódot és meg kell fogalmaznunk a megfelelő hibaüzenetet. Az RFC7807 alkalmazásával strukturált hibaüzenetek küldhetők, ami segíti a klienst a hiba megértésében és kezelésében. Ezekkel a módszerekkel a Go nyelven írt HTTP API-k hibakezelése egyszerűbbé és egységesebbé tehető.

A cikk kódja a [gosuda/httpwrap](https://github.com/gosuda/httpwrap) repozitóriumban érhető el.
