---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Å håndtere HTTP-feil på en mindre tungvint måte + RFC7807
description: Synes du feilhåndtering i Go-API-er er tungvint? Håndter feil på en enklere og tydeligere måte med RFC7807.
language: "no"
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Oversikt

Når man lager et HTTP API i Go, er det mest plagsomme feilhåndteringen. Et typisk eksempel er denne koden.

```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 antallet API-er er lite, vil det ikke være særlig upraktisk å skrive koden på denne måten. Men etter hvert som antallet API-er øker og den interne logikken blir mer kompleks, vil tre forhold bli irriterende.

1. Retur av passende feilkode
2. Mange loggføringer
3. Sending av tydelige feilmeldinger

## Hovedinnhold

### Retur av passende feilkode

Punkt 1, retur av passende feilkode, er riktignok en personlig frustrasjon for meg. En erfaren utvikler vil finne den passende koden og sette den inn riktig hver gang, men både jeg og mindre erfarne utviklere kan ha vanskeligheter med å bruke passende feilkoder konsekvent når logikken blir kompleks og antallet kall øker. Det finnes flere tilnærminger til dette, og den mest vanlige er å på forhånd designe API-ets logiske flyt og deretter skrive koden slik at den returnerer passende feil. ~~Gjør slik~~

Men dette virker ikke som den optimale løsningen for en menneskelig utvikler som har hjelp av IDE (eller Language Server). Ettersom REST API i seg selv utnytter betydningen i feilkodene maksimalt, kan man foreslå en annen tilnærming. Man lager en ny implementasjon av grensesnittet `error`, kalt `HttpError`, som lagrer `StatusCode` og `Message`. Deretter tilbyr man følgende hjelpefunksjon.

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

Hjelpefunksjonen `BadRequest` vil returnere en `HttpError` der `StatusCode` er satt til 400 og `Message` til verdien som sendes inn som argument. I tillegg kan man selvsagt lage og finne hjelpefunksjoner som `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired` og lignende via autofullføring. Dette er raskere enn å se opp i et ferdig designdokument, og det er mer pålitelig enn å skrive feilkoden som tall hver gang. ~~Står ikke alt i konstantene `http.StatusCode`? Hysj~~

### Antall loggføringer

Ved feil vil man naturligvis ønske å logge. Når man logger at et API er kalt og om forespørselen lyktes eller mislyktes, blir det mange linjer kode om man skal logge ved alle mulige avslutningspunkter fra starten. Ved å pakke inn selve handleren kan man håndtere dette sentralt.

Følgende er et eksempel på innpakning av `chi`-ruteren.

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

Ruterstrukturen inneholder `chi.Router` internt, slik at funksjonaliteten til `chi.Router` kan brukes direkte. I metoden `Get` sjekkes det om hjelpefunksjonen som er foreslått ovenfor returnerer en `HttpError`-struct; i så fall returneres den på riktig måte. Når det er en annen `error`, sendes den samlet til feiltilbakekallingsfunksjonen. Denne tilbakekallingen mottas via konstruktøren.

Følgende er kode skrevet med denne pakken.

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

Oppsummert: Hvis man bare lager `HttpError` via hjelpefunksjoner og returnerer den, kan man i øverste nivå svare med passende feilkode og melding, samtidig som man registrerer en tilbakekalling som logger på en måte som passer for hver tjeneste. Om nødvendig kan man utvide dette med for eksempel `RequestID` for mer detaljert loggføring.

### Sending av tydelige feilmeldinger

For dette finnes dokumentet RFC7807. RFC7807 definerer og bruker hovedsakelig følgende elementer.

- `type`: en URI som identifiserer feiltypen. Dette er primært et dokument som beskriver feilen.
- `title`: en kort, ettlinjes beskrivelse av hvilken feil det er.
- `status`: samme som HTTP Status Code.
- `detail`: en detaljert beskrivelse av feilen som er lesbar for mennesker.
- `instance`: URI-en der feilen oppstod. For eksempel, hvis feilen oppstod ved `GET /user/info`, vil verdien være `/user/info`.
- `extensions`: tilleggselementer i form av et JSON-objekt som beskriver feilen nærmere.
  - For eksempel kan brukerens input inngå når det gjelder `BadRequest`.
  - Eller man kan inkludere tidspunktet for den siste forespørselen når det gjelder `TooManyRequest`.

For å gjøre dette enkelt å bruke, lager man en ny fil i pakken `httperror`, samme sted som `HttpError`, oppretter en struct `RFC7807Error` og gjør den tilgjengelig for konstruksjon med method chaining.

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

`Type`-verdien `"about:blank"` er standardverdi. Den betyr en side som ikke finnes. Nedenfor er et eksempel på å lage en feil for ugyldig forespørsel.

```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 enkel method chaining kan man lage en strukturert feilmelding for brukeren. Videre kan man støtte følgende metode for å bruke den sentraliserte ruteren som ble laget tidligere.

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

Ved å bruke dette direkte og endre eksempelet ovenfor, får man 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())
}
```

## Konklusjon

Ved å håndtere feil via en sentralisert ruter, blir det mindre arbeid å sjekke feilkoder og skrive passende feilmeldinger hver gang. Ved å bruke RFC7807 til å gi strukturerte feilmeldinger, hjelper man klienten med å forstå og håndtere feilen. Med disse metodene kan feilhåndteringen i HTTP API-er skrevet i Go gjøres enklere og mer konsistent.

Koden i denne artikkelen finnes i repositoriet [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
