---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Att hantera HTTP-fel på ett smidigare sätt + RFC7807
description: Tycker du att felhanteringen i ditt Go API är besvärlig? Hantera den enklare och tydligare med RFC7807.
language: sv
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Översikt

När man skapar ett HTTP API i Go är felhanteringen det mest besvärliga. Ett typiskt exempel ser ut så här.

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

Om antalet API:er är litet går det att skriva på det här sättet utan att det känns särskilt besvärligt. När antalet API:er ökar och den interna logiken blir mer komplex blir dock tre saker irriterande.

1. Att returnera lämpliga felkoder
2. Antalet loggrader som måste skrivas
3. Att skicka tydliga felmeddelanden

## Huvuddel

### Att returnera lämpliga felkoder

Att returnera lämpliga felkoder är visserligen ett personligt klagomål från min sida. En erfaren utvecklare hittar säkert rätt kod och använder den konsekvent varje gång, men jag och andra mindre erfarna utvecklare kan ha svårt att konsekvent använda rätt felkod när logiken blir komplex och antalet anrop ökar. Det finns flera sätt att hantera detta, och det vanligaste är att i förväg designa API:ts logikflöde och sedan skriva koden så att rätt fel returneras. ~~Gör så här~~

Detta verkar dock inte vara det optimala tillvägagångssättet för en mänsklig utvecklare som får hjälp av en IDE (eller en Language Server). Eftersom REST API:er i sig utnyttjar den betydelse som ligger i felkoderna så fullt som möjligt, kan man dessutom föreslå ett annat tillvägagångssätt. Skapa en ny implementation av gränssnittet `error`, kallad `HttpError`, som lagrar `StatusCode` och `Message`. Tillhandahåll sedan följande hjälpfunktioner.

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

Hjälpfunktionen `BadRequest` returnerar ett `HttpError` där `StatusCode` är satt till 400 och `Message` till det värde som skickats som argument. Utöver detta kan man naturligtvis lägga till och söka efter hjälpfunktioner som `NotImplement`, `ServiceUnavailable`, `Unauthorized` och `PaymentRequired` med hjälp av autokomplettering. Detta går snabbare än att kontrollera en förberedd designspecifikation varje gång, och det är mer tillförlitligt än att skriva in felkoden som en siffra varje gång. ~~Säger du att allt finns i konstanterna i `http.StatusCode`? Shh~~

### Antalet loggrader som måste skrivas

Vid fel loggar man naturligtvis. När man loggar om ett API har anropats och om en begäran lyckades eller misslyckades, blir det mycket kod om man skriver loggar vid varje förväntad slutpunkt från början. Genom att omsluta själva handlaren kan man i stället hantera detta centralt.

Följande är ett exempel där `chi`-routern omsluts.

```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 innehåller `chi.Router` internt, så att `chi.Router`:s funktioner kan användas direkt. I metoden `Get` kontrolleras om det returnerade värdet är en `HttpError`-struktur, som returneras av de hjälpfunktioner vi just föreslog, och den returneras på lämpligt sätt. Om det är ett `error` skickas det enhetligt vidare till felcallbacken. Denna callback tas emot via konstruktorn.

Följande är kod skriven med hjälp av detta paket.

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

Vad tycker du? Om man bara skapar `HttpError` som hjälpfunktioner och returnerar dem, kan den övre nivån svara med lämplig felkod och ett lämpligt meddelande, och man kan registrera en callback för att skriva lämpliga loggar i varje tjänst. Vid behov kan detta utökas med till exempel `RequestID` för mer detaljerad loggning.

### Att skicka tydliga felmeddelanden

För detta finns dokumentet RFC7807. RFC7807 definierar och använder huvudsakligen följande element.

- `type`: en URI som identifierar feltypen. Det är i huvudsak ett dokument som beskriver felet.
- `title`: en kort, enradig beskrivning av vilket fel det är.
- `status`: samma som HTTP-statuskoden.
- `detail`: en detaljerad beskrivning av felet som kan läsas av människor.
- `instance`: URI:n där felet uppstod. Om felet till exempel uppstod vid `GET /user/info` blir värdet `/user/info`.
- `extensions`: ett sekundärt element i form av ett JSON-objekt för att beskriva felet närmare.
  - Till exempel kan användarens indata ingå vid `BadRequest`.
  - Eller så kan den senaste förfrågningstidpunkten inkluderas vid `TooManyRequest`.

För att göra detta enkelt att använda skapar man en ny fil i paketet `httperror`, samma plats som `HttpError`, definierar en struktur `RFC7807Error` och låter den skapas med ett metodkedjemönster.

```go
func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
	return &RFC7807Error{
		Type:   "about:blank", // Standardtyp enligt 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"` för `Type` är standardvärdet. Det betyder en sida som inte finns. Nedan följer ett exempel på att skapa ett fel för en felaktig begäran.

```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 metodkedjning kan man skapa strukturerade felmeddelanden för användaren. För att även kunna använda den centraliserade routern som skapades tidigare kan man dessutom stödja följande metod.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Om marshalling misslyckas, använd endast detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Om vi använder detta direkt och ändrar exemplet ovan blir det så här.

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

## Slutsats

Genom att hantera fel med en centraliserad router minskar belastningen av att kontrollera felkoder och skriva lämpliga felmeddelanden varje gång. Genom att använda RFC7807 för att skicka strukturerade felmeddelanden kan man dessutom hjälpa klienten att förstå och hantera felen. På så sätt kan felhanteringen i HTTP API:er skrivna i Go göras enklare och mer konsekvent.

Koden i den här artikeln finns i förvaringsplatsen [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
