---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Gestionarea mai puțin anevoioasă a erorilor HTTP + RFC7807
description: Vă este greu să gestionați erorile din API-ul Go? Încercați RFC7807 pentru a le trata într-un mod mai simplu și mai clar.
language: ro
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Prezentare generală

Atunci când creați un API HTTP în limbajul Go, cel mai supărător lucru este gestionarea erorilor. Un exemplu tipic este următorul cod.

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

Dacă numărul de API-uri este redus, scrierea în acest mod nu va fi, în mod real, incomodă. Totuși, pe măsură ce numărul API-urilor crește și logica internă devine mai complexă, trei aspecte încep să devină problematice.

1. Returnarea unui cod de eroare adecvat
2. Numărul mare de scrieri de jurnal (log) asociate rezultatelor
3. Transmiterea unui mesaj de eroare clar

## Dezvoltare

### Returnarea unui cod de eroare adecvat

Desigur, primul punct, returnarea unui cod de eroare adecvat, reprezintă o nemulțumire personală. Un dezvoltator experimentat va găsi codul potrivit și îl va introduce corect de fiecare dată, însă eu, și la fel dezvoltatorii încă neexperimentați, pot întâmpina dificultăți în utilizarea regulată a codurilor de eroare potrivite, pe măsură ce logica devine mai complexă și numărul apelurilor crește. Pentru aceasta există mai multe metode, iar cea mai reprezentativă ar fi proiectarea fluxului logic al API-ului în prealabil și apoi scrierea codului care returnează erorile corespunzătoare. ~~Faceți așa~~

Totuși, această abordare nu pare optimă pentru un dezvoltator uman care beneficiază de ajutorul IDE-ului (sau al Language Server-ului). De asemenea, deoarece REST API utilizează în mod intrinsec semnificația codurilor de eroare în măsură maximă, se poate propune o altă metodă. Se creează o implementare nouă a interfeței de eroare (`error`), numită `HttpError`, care stochează `StatusCode` și `Message`. Apoi se oferă funcții auxiliare de tip helper, precum cele de mai jos.

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

Funcția helper `BadRequest` va returna un `HttpError` în care `StatusCode` este setat la 400, iar `Message` la valoarea primită ca argument. Pe lângă acestea, se pot adăuga și consulta, desigur, funcții helper precum `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired` prin funcția de completare automată. Aceasta este mai rapidă decât consultarea repetată a unui proiect de design pregătit și mai sigură decât introducerea manuală, de fiecare dată, a codurilor de eroare sub formă numerică. ~~Toate sunt în constantele `http.StatusCode`? Șșt~~

### Numărul mare de scrieri de jurnal (log) asociate rezultatelor

La apariția unei erori, este firesc să se lase un jurnal. Atunci când se înregistrează dacă un apel API a reușit sau a eșuat, scrierea unui jurnal la fiecare punct de ieșire previzibil, începând de la început, duce la creșterea numărului de linii de cod. Prin învelirea întregului handler, această gestionare poate fi centralizată.

Urmează un exemplu de învelire a routerului `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)
		}
	})
}
```

La structura routerului, `chi.Router` este păstrat în interior, astfel încât funcționalitățile lui `chi.Router` sunt utilizate exact în forma lor originală. Analizând metoda `Get`, se verifică dacă a fost returnată structura `HttpError` generată de funcțiile helper propuse mai sus, și apoi se răspunde în mod corespunzător; în cazul unui `error` obișnuit, acesta este transmis uniform către funcția de callback pentru erori. Acest callback este primit prin constructor.

Urmează codul scris folosind acest pachet.

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

Ce părere aveți? Dacă se creează doar `HttpError` ca funcție helper și se returnează, nivelul superior poate răspunde cu codul de eroare și mesajul potrivite, iar prin înregistrarea unui callback, fiecare serviciu implementat poate scrie jurnalul corespunzător. Suplimentar, dacă este necesar, se poate extinde sistemul și folosi `RequestID` și alte elemente pentru o înregistrare detaliată a jurnalelor.

### Transmiterea unui mesaj de eroare clar

Pentru aceasta există documentul RFC7807. RFC7807 definește și utilizează, în principal, următoarele elemente.

- `type`: URI care identifică tipul erorii. De regulă, este un document care descrie eroarea.
- `title`: O descriere scurtă, pe o singură linie, a erorii.
- `status`: Identic cu codul de stare HTTP.
- `detail`: O descriere detaliată a erorii, lizibilă pentru om.
- `instance`: URI-ul la care a apărut eroarea. De exemplu, dacă eroarea a apărut la `GET /user/info`, valoarea va fi `/user/info`.
- `extensions`: Element secundar, structurat ca obiect JSON, destinat descrierii erorii.
  - De exemplu, în cazul `BadRequest`, pot fi incluse datele introduse de utilizator.
  - Sau, în cazul `TooManyRequest`, poate fi inclus momentul cel mai recent al cererii.

Pentru a facilita utilizarea, în pachetul `httperror`, în același loc cu `HttpError`, se creează un fișier nou, se definește structura `RFC7807Error` și se permite crearea acesteia prin modelul de înlănțuire a metodelor (method chaining).

```go
func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
	return &RFC7807Error{
		Type:   "about:blank", // Default type as per 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 { ... }
```

Valoarea `"about:blank"` a câmpului `Type` este cea implicită. Ea semnifică o pagină inexistentă. Mai jos este un exemplu de creare a unei erori pentru o cerere incorectă.

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

Prin utilizarea simplă a metodelor înlănțuite, se poate genera un mesaj de eroare structurat pentru utilizator. De asemenea, pentru a utiliza routerul centralizat creat anterior, se poate oferi suport prin următoarea metodă.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// If marshaling fails, fall back to just using the detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Folosind direct această metodă și modificând exemplul de mai sus, se obține următorul rezultat.

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

## Concluzie

Prin gestionarea erorilor cu ajutorul unui router centralizat, povara verificării codurilor de eroare și a redactării unor mesaje de eroare adecvate de fiecare dată este redusă. De asemenea, prin utilizarea RFC7807 pentru a furniza mesaje de eroare structurate, se poate sprijini clientul în înțelegerea și tratarea erorilor. Prin aceste metode, gestionarea erorilor în cadrul API-urilor HTTP scrise în limbajul Go poate deveni mai simplă și mai coerentă.

Codul din acest articol poate fi consultat în repository-ul [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
