---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Mere elegant håndtering af HTTP-fejl + RFC7807
description: Er fejlhåndtering i din Go API besværlig? Håndter fejl mere enkelt og tydeligt med RFC7807.
language: da
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Oversigt

Når man opretter en HTTP API i Go, er fejlhåndtering det mest besværlige. Et typisk eksempel på en sådan kode ser sådan ud.

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

Hvis der kun er få API'er, er der sandsynligvis ingen større ulempe ved at skrive koden på denne måde. Men efterhånden som antallet af API'er vokser og den interne logik bliver mere kompleks, begynder tre ting at irritere:

1. Returnering af passende fejlkoder
2. Det store antal logposter
3. Afsendelse af tydelige fejlmeddelelser

## Hovedindhold

### Returnering af passende fejlkoder

Punkt 1, returnering af passende fejlkoder, er ganske vist en personlig frustration for mig. En erfaren udvikler finder altid den rette kode og indsætter den korrekt hver gang. Men jeg selv og andre mindre erfarne udviklere kan have svært ved konsekvent at anvende de rette fejlkoder, efterhånden som logikken bliver mere kompleks og antallet af kald stiger. Der findes sikkert flere metoder til dette, og den mest almindelige er at designe API'ens logiske flow på forhånd og derefter skrive koden, så den returnerer den passende fejl. ~~Gør bare sådan~~

Men dette forekommer ikke at være den optimale metode for en menneskelig udvikler, der får hjælp fra en IDE (eller Language Server). Da selve REST API'et udnytter betydningen i fejlkoderne så meget som muligt, kan man desuden foreslå en anden tilgang. Man opretter en ny implementering af `error`-interfacet kaldet `HttpError`, som gemmer `StatusCode` og `Message`. Derefter stilles følgende hjælpefunktioner til rådighed.

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

Hjælpefunktionen `BadRequest` returnerer en `HttpError`, hvor `StatusCode` er sat til 400, og `Message` er sat til den modtagne værdi. Ud over dette kan hjælpefunktioner som `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired` og lignende naturligvis slås op og tilføjes via autofuldførelse. Dette er hurtigere end at slå op i et færdigt designdokument hver gang, og mere pålideligt end at indtaste fejlkoder som tal hver gang. ~~Er det ikke alt sammen i konstanterne i `http.StatusCode`? Shh~~

### Antallet af logposter

Ved fejl logger man naturligvis. Når man logger, om et API-kald er lykkedes eller mislykkedes, bliver koden længere, hvis man logger på alle forventede afslutningspunkter fra start. Ved at indpakke selve handleren kan man i stedet administrere logningen centralt.

Følgende er et eksempel, der indpakker `chi`-routeren.

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

Routerstrukturen indeholder `chi.Router` internt, så `chi.Router`s funktionalitet kan anvendes direkte. I `Get`-metoden kontrolleres det, om der returneres en `HttpError`-struktur fra hjælpefunktionen, som blev foreslået ovenfor. Hvis det er tilfældet, returneres den passende fejl. Hvis det er en almindelig `error`, sendes den samlet til fejl-callback-funktionen. Denne callback modtages gennem konstruktøren.

Følgende er kode, der anvender denne pakke.

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

Hvad synes du? Hvis man blot opretter `HttpError` via hjælpefunktioner og returnerer dem, kan det øverste scope svare med den passende fejlkode og meddelelse. Samtidig kan man registrere en callback, der logger passende for hver implementeringstjeneste. Hvis det er nødvendigt, kan dette udvides, så der kan foretages detaljeret logning ved hjælp af fx `RequestID`.

### Afsendelse af tydelige fejlmeddelelser

Dokumentet RFC7807 er en standard til dette formål. RFC7807 definerer og anvender primært følgende elementer:

- `type`: en URI, der identificerer fejltypen. Det er primært et dokument, der beskriver fejlen.
- `title`: en kort beskrivelse på en linje af, hvilken fejl der er tale om.
- `status`: svarer til HTTP-statuskoden.
- `detail`: en detaljeret forklaring af fejlen, læsbar for mennesker.
- `instance`: URI'en, hvor fejlen opstod. Hvis fejlen for eksempel opstod ved `GET /user/info`, vil værdien være `/user/info`.
- `extensions`: sekundære elementer i form af et JSON-objekt, der beskriver fejlen yderligere.
  - For eksempel kan brugerens input indgå ved `BadRequest`.
  - Eller ved `TooManyRequest` kan tidspunktet for den seneste anmodning indgå.

For at gøre dette let at bruge oprettes en ny fil i pakken `httperror`, samme sted som `HttpError`. Der defineres en struktur `RFC7807Error`, som kan oprettes med et metodekæde-mønster.

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

`"about:blank"` i `Type` er standardværdien. Den betyder en side, der ikke findes. Nedenfor er et eksempel på oprettelse af en fejl ved en ugyldig anmodning.

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

Med et simpelt metodekæde kan man oprette en struktureret fejlmeddelelse til brugeren. For at bruge den centraliserede router, der blev introduceret tidligere, kan man desuden tilbyde følgende metode.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Hvis marshaling mislykkes, anvendes blot detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Ved at anvende dette direkte og ændre eksemplet ovenfor bliver det til følgende.

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

## Konklusion

Når fejl håndteres via denne centraliserede router, reduceres byrden ved at skulle kontrollere fejlkoder og skrive passende fejlmeddelelser hver gang. Ved at anvende RFC7807 til at levere strukturerede fejlmeddelelser kan man desuden hjælpe klienten med at forstå og håndtere fejlene. På denne måde kan fejlhåndteringen i HTTP API'er skrevet i Go gøres enklere og mere konsistent.

Koden i denne artikel kan findes i depotet [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
