---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Virheiden käsittelyn helpottaminen HTTP-virheissä + RFC7807
description: Ovatko Go API:n virheenkäsittelyn vaivalloisuudet vaivaavat? Käsittele virheet RFC7807-standardin avulla yksinkertaisemmin ja selkeämmin.
language: fi
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Yleiskatsaus

Go-kielellä HTTP-rajapintoja luotaessa suurin vaiva on virheiden käsittely. Tyypillisesti koodi kirjoitetaan tällaisena:

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

Jos rajapintoja on vain muutama, tällainen kirjoitustapa ei aiheuta erityistä haittaa. Kun rajapintojen määrä kuitenkin kasvaa ja sisäinen logiikka monimutkaistuu, kolme asiaa alkaa ärsyttää.

1. Sopivan virhekoodin palauttaminen
2. Lokikirjausten suuri määrä
3. Selkeän virheviestin lähettäminen

## Pääosa

### Sopivan virhekoodin palauttaminen

Ensimmäinen kohta, sopivan virhekoodin palauttaminen, on tosin pelkästään minun henkilökohtainen valitukseni. Kokenut kehittäjä löytää sopivan koodin ja lisää sen aina oikein, mutta minulle ja vielä kokemattomille kehittäjille voi olla vaikeaa käyttää sopivia virhekoodeja johdonmukaisesti, kun logiikka monimutkaistuu ja kutsujen määrä kasvaa. Tähän on useita ratkaisuja, joista tyypillisin on suunnitella rajapinnan logiikkavirta etukäteen ja kirjoittaa koodi palauttamaan sopiva virhe. ~~Tehkää niin~~

Tämä ei kuitenkaan vaikuta optimaaliselta tavalta ihmiskehittäjälle, joka käyttää IDE:tä (tai Language Serveriä). Koska REST-rajapinta itsessään hyödyntää virhekoodeihin sisältyvää merkitystä mahdollisimman tehokkaasti, voidaan ehdottaa myös toista lähestymistapaa. Luodaan uusi `error`-rajapinnan toteutus nimeltä `HttpError`, joka tallentaa `StatusCode`- ja `Message`-kentät. Lisäksi tarjotaan seuraavanlaisia apufunktioita.

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

Funktio `BadRequest` palauttaa `HttpError`-olion, jonka `StatusCode` on 400 ja `Message` on asetettu argumentin arvoon. Muita apufunktioita, kuten `NotImplement`, `ServiceUnavailable`, `Unauthorized` ja `PaymentRequired`, voidaan tietysti lisätä ja hakea automaattisen täydennyksen avulla. Tämä on nopeampaa kuin tarkistaa valmis suunnitteludokumentti joka kerta, ja se on luotettavampaa kuin syöttää virhekoodi numerona joka kerta. ~~Kaikkihan on `http.StatusCode`-vakioissa, sanotte? Pst~~

### Lokikirjausten suuri määrä

Virheen sattuessa lokia kirjoitetaan luonnollisesti. Kun kirjataan, onnistuiko vai epäonnistuiko rajapinnan kutsu, lokin kirjaaminen jokaiseen ennakoituun lopetuskohtaan alusta alkaen lisää kirjoitettavan koodin määrää. Käärimällä itse käsittelijän voidaan hallita tätä keskitetysti.

Seuraavassa on esimerkki `chi`-reitittimen käärimisestä.

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

Reitittimen rakenne sisältää `chi.Router`-olion, joten `chi.Router`:n toiminnallisuus on käytettävissä sellaisenaan. `Get`-metodissa tarkistetaan, onko palautettu virhe edellä ehdotetun apufunktion palauttama `HttpError`-rakenne. Jos on, se palautetaan sopivasti, ja muussa tapauksessa kaikki `error`-virheet välitetään yhtenäisesti virhe-callback-funktiolle. Tämä callback saadaan konstruktorin kautta.

Seuraavassa on koodi, joka on kirjoitettu tätä pakettia hyödyntäen.

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

Mitä mieltä olette? Jos luodaan pelkästään `HttpError` apufunktioina ja palautetaan se, ylemmän tason scope voi palauttaa vastauksen sopivalla virhekoodilla ja viestillä. Jokaiselle palvelulle voidaan lisäksi rekisteröidä callback, joka kirjaa sopivat lokit. Tarvittaessa tätä voidaan laajentaa esimerkiksi `RequestID`-tunnisteilla, jotta lokitus on yksityiskohtaisempaa.

### Selkeän virheviestin lähettäminen

Tähän tarkoitukseen on olemassa dokumentti RFC7807. RFC7807 määrittelee pääasiassa seuraavat elementit:

- `type`: URI, joka tunnistaa virhetyypin. Yleensä se on dokumentti, joka kuvaa virhettä.
- `title`: lyhyt yhden rivin kuvaus siitä, minkä virheen kyseessä on.
- `status`: sama kuin HTTP-statuskoodi.
- `detail`: ihmisen luettavissa oleva yksityiskohtainen kuvaus kyseisestä virheestä.
- `instance`: URI, jossa virhe tapahtui. Esimerkiksi jos virhe tapahtui kohdassa `GET /user/info`, arvo on `/user/info`.
- `extensions`: JSON-oliona muodostuva virhettä kuvaava lisäelementti.
  - Esimerkiksi `BadRequest`-tapauksessa siihen voidaan sisällyttää käyttäjän syöte.
  - Tai `TooManyRequest`-tapauksessa siihen voidaan sisällyttää viimeisimmän pyynnön ajankohta.

Jotta tätä olisi helppo käyttää, luodaan `httperror`-pakettiin, samaan paikkaan kuin `HttpError`, uusi tiedosto, jossa määritellään `RFC7807Error`-rakenne. Lisäksi sen luominen mahdollistetaan metodiketjutusmallilla.

```go
func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
	return &RFC7807Error{
		Type:   "about:blank", // Oletustyyppi RFC7807:n mukaisesti
		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`-kentän `"about:blank"` on oletusarvo, ja se tarkoittaa tyhjää sivua. Alla on esimerkki väärään pyyntöön liittyvän virheen luomisesta.

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

Yksinkertaisella metodiketjutuksella voidaan luoda käyttäjälle jäsennelty virheviesti. Lisäksi, jotta aiemmin kirjoitettua keskitettyä reititintä voidaan hyödyntää, voidaan tukea seuraavaa metodia.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Jos marshalointi epäonnistuu, käytetään pelkästään detail-kenttää
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Käytetään tätä sellaisenaan ja muokataan yllä olevaa esimerkkiä, jolloin saadaan seuraava tulos.

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

## Johtopäätös

Kun virheitä käsitellään tällaisella keskitetyllä reitittimellä, voidaan vähentää taakkaa, joka liittyy virhekoodien tarkistamiseen ja sopivien virheviestien kirjoittamiseen joka kerta. Lisäksi RFC7807:n käyttö jäsenneltyjen virheviestien tarjoamiseen auttaa asiakasta ymmärtämään virheet ja käsittelemään ne. Näiden menetelmien avulla Go-kielellä kirjoitetun HTTP-rajapinnan virheenkäsittelystä voidaan tehdä entistä yksinkertaisempaa ja yhtenäisempää.

Tämän kirjoituksen koodi on saatavilla [gosuda/httpwrap](https://github.com/gosuda/httpwrap) -repositoriossa.
