GoSuda

Penanganan Kesalahan HTTP yang Lebih Mudah + RFC7807

By snowmerak
views ...

κ°œμš”

Dalam bahasa Go, ketika membuat http API, penanganan kesalahan adalah hal yang paling merepotkan. Secara umum, ada kode seperti ini.

1func(w http.ResponseWriter, r *http.Request) {
2    err := doSomething()
3    if err != nil {
4        http.Error(w, err.Error(), http.StatusInternalServerError)
5        log.Printf("error occurred: %v", err)
6        return
7    }
8    // ...
9}

Jika tidak banyak API, menulis dengan cara ini mungkin tidak akan menimbulkan ketidaknyamanan. Namun, seiring bertambahnya jumlah API dan semakin kompleksnya logika internal, tiga hal berikut menjadi mengganggu.

  1. Pengembalian kode kesalahan yang sesuai
  2. Banyaknya penulisan log hasil
  3. Pengiriman pesan kesalahan yang jelas

λ³Έλ‘ 

μ μ ˆν•œ μ—λŸ¬ μ½”λ“œ λ°˜ν™˜

Tentu saja, poin 1, pengembalian kode kesalahan yang sesuai, adalah keluhan pribadi saya. Pengembang yang berpengalaman mungkin akan selalu menemukan dan menggunakan kode yang sesuai, tetapi saya dan pengembang yang belum mahir mungkin mengalami kesulitan dalam menggunakan kode kesalahan yang tepat secara teratur seiring dengan kompleksitas logika dan meningkatnya jumlah pengulangan. Ada beberapa metode untuk hal ini, dan yang paling umum adalah merancang alur logika API terlebih dahulu, kemudian menulis kode untuk mengembalikan kesalahan yang sesuai. Lakukanlah itu

Namun, ini tampaknya bukan metode yang optimal bagi pengembang manusia yang dibantu oleh IDE (atau Language Server). Selain itu, karena REST API sendiri memanfaatkan makna yang terkandung dalam kode kesalahan semaksimal mungkin, metode lain dapat diusulkan. Buatlah implementasi antarmuka error baru yang disebut HttpError untuk menyimpan StatusCode dan Message. Kemudian sediakan fungsi pembantu berikut.

1err := httperror.BadRequest("wrong format")

Fungsi pembantu BadRequest akan mengembalikan HttpError dengan StatusCode 400 dan Message diatur ke nilai yang diterima sebagai argumen. Selain itu, tentu saja, fungsi pembantu seperti NotImplement, ServiceUnavailable, Unauthorized, PaymentRequired dapat dicari dan ditambahkan melalui fitur auto-complete. Ini akan lebih cepat daripada memeriksa dokumen desain yang sudah disiapkan setiap saat, dan akan lebih stabil daripada memasukkan kode kesalahan berupa angka setiap saat. http.StatusCode ada di konstanta? Sst

λ§Žμ€ κ²°κ³Ό 둜그 μž‘μ„± 수

Ketika terjadi kesalahan, log secara alami akan dicatat. Ketika API dipanggil, dan log dicatat apakah permintaan berhasil atau gagal, mencatat log dari awal hingga semua titik keluar yang diharapkan akan meningkatkan jumlah kode yang harus ditulis. Ini dapat dikelola secara terpusat dengan membungkus handler itu sendiri.

Berikut adalah contoh pembungkus router chi.

 1package chiwrap
 2
 3import (
 4	"errors"
 5	"net/http"
 6
 7	"github.com/go-chi/chi/v5"
 8
 9	"github.com/gosuda/httpwrap/httperror"
10)
11
12type Router struct {
13	router      chi.Router
14	errCallback func(err error)
15}
16
17func NewRouter(errCallback func(err error)) *Router {
18	if errCallback == nil {
19		errCallback = func(err error) {}
20	}
21	return &Router{
22		router:      chi.NewRouter(),
23		errCallback: errCallback,
24	}
25}
26
27type HandlerFunc func(writer http.ResponseWriter, request *http.Request) error
28
29func (r *Router) Get(pattern string, handler HandlerFunc) {
30	r.router.Get(pattern, func(writer http.ResponseWriter, request *http.Request) {
31		if err := handler(writer, request); err != nil {
32			he := &httperror.HttpError{}
33			switch errors.As(err, &he) {
34			case true:
35				http.Error(writer, he.Message, he.Code)
36			case false:
37				http.Error(writer, err.Error(), http.StatusInternalServerError)
38			}
39			r.errCallback(err)
40		}
41	})
42}

Struktur Router memiliki chi.Router di dalamnya, sehingga dikonfigurasi untuk menggunakan fungsionalitas chi.Router secara langsung. Dalam metode Get, setelah memeriksa apakah struktur HttpError yang dikembalikan oleh fungsi pembantu yang baru saja saya usulkan di atas telah dikembalikan, ia akan mengembalikannya dengan sesuai, dan jika itu adalah error, ia akan menyerahkannya ke fungsi callback kesalahan secara seragam. Callback ini diterima melalui konstruktor.

Berikut adalah kode yang ditulis menggunakan paket ini.

 1package main
 2
 3import (
 4	"bytes"
 5	"context"
 6	"errors"
 7	"io"
 8	"log"
 9	"net/http"
10
11	"github.com/gosuda/httpwrap/httperror"
12	"github.com/gosuda/httpwrap/wrapper/chiwrap"
13)
14
15func main() {
16    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
17    defer cancel()
18
19	r := chiwrap.NewRouter(func(err error) {
20		log.Printf("Router log test: Error occured: %v", err)
21	})
22	r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
23		name := request.URL.Query().Get("name")
24		if name == "" {
25			return httperror.BadRequest("name is required")
26		}
27
28		writer.Write([]byte("Hello " + name))
29		return nil
30	})
31
32	svr := http.Server{
33		Addr:    ":8080",
34		Handler: r,
35	}
36	go func() {
37		if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
38			log.Fatalf("Failed to start server: %v", err)
39		}
40	}()
41
42    <-ctx.Done()
43    svr.Shutdown(context.Background())
44}

Bagaimana menurut Anda? Jika hanya HttpError yang dibuat sebagai fungsi pembantu dan dikembalikan, maka dalam cakupan yang lebih tinggi, respons dapat dikembalikan dengan kode kesalahan dan pesan yang sesuai, dan callback dapat didaftarkan untuk mencatat log yang sesuai untuk setiap layanan implementasi. Selain itu, jika diperlukan, dapat diperluas untuk memungkinkan pencatatan log yang lebih rinci menggunakan RequestID, dan lain-lain.

λͺ…ν™•ν•œ μ—λŸ¬ λ©”μ‹œμ§€ 전솑

Dokumen untuk ini adalah RFC7807. RFC7807 terutama menggunakan elemen-elemen berikut:

  • type: URI yang mengidentifikasi jenis kesalahan. Ini biasanya merupakan dokumen yang menjelaskan kesalahan tersebut.
  • title: Deskripsi singkat tentang jenis kesalahan.
  • status: Sama dengan HTTP Status Code.
  • detail: Deskripsi rinci yang dapat dibaca manusia tentang kesalahan tersebut.
  • instance: URI tempat kesalahan terjadi. Misalnya, jika kesalahan terjadi di GET /user/info, maka /user/info akan menjadi nilainya.
  • extensions: Elemen sekunder untuk menjelaskan kesalahan, disusun dalam bentuk JSON Object.
    • Misalnya, dalam kasus BadRequest, input pengguna mungkin disertakan.
    • Atau, dalam kasus TooManyRequest, waktu permintaan terakhir mungkin disertakan.

Untuk memudahkan penggunaannya, buat file baru di paket httperror pada lokasi yang sama dengan HttpError, buat struktur RFC7807Error, dan aktifkan pembuatannya dengan pola method chaining.

 1func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
 2	return &RFC7807Error{
 3		Type:   "about:blank", // Default type as per RFC7807
 4		Title:  title,
 5		Status: status,
 6		Detail: detail,
 7	}
 8}
 9
10func BadRequestProblem(detail string, title ...string) *RFC7807Error {
11	t := "Bad Request"
12	if len(title) > 0 && title[0] != "" {
13		t = title[0]
14	}
15	return NewRFC7807Error(http.StatusBadRequest, t, detail)
16}
17
18func (p *RFC7807Error) WithType(typeURI string) *RFC7807Error { ... }
19func (p *RFC7807Error) WithInstance(instance string) *RFC7807Error { ... }
20func (p *RFC7807Error) WithExtension(key string, value interface{}) *RFC7807Error { ... }

"about:blank" untuk Type adalah nilai default. Ini berarti halaman yang tidak ada. Berikut adalah contoh pembuatan kesalahan untuk permintaan yang salah.

1problem := httperror.BadRequestProblem("invalid user id format", "Bad User Input")
2
3problem = problem.WithType("https://example.com/errors/validation")
4         .WithInstance("/api/users/abc")
5         .WithExtension("invalid_field", "user_id")
6         .WithExtension("expected_format", "numeric")

Dengan method chaining sederhana, pesan kesalahan terstruktur untuk pengguna dapat dibuat. Selain itu, untuk menggunakan router terpusat yang telah ditulis di atas, metode berikut dapat didukung.

1func (p *RFC7807Error) ToHttpError() *HttpError {
2	jsonBytes, err := json.Marshal(p)
3	if err != nil {
4		// If marshaling fails, fall back to just using the detail
5		return New(p.Status, p.Detail)
6	}
7	return New(p.Status, string(jsonBytes))
8}

Jika contoh di atas dimodifikasi menggunakan ini, hasilnya akan seperti ini.

 1package main
 2
 3import (
 4	"bytes"
 5	"context"
 6	"errors"
 7	"io"
 8	"log"
 9	"net/http"
10
11	"github.com/gosuda/httpwrap/httperror"
12	"github.com/gosuda/httpwrap/wrapper/chiwrap"
13)
14
15func main() {
16    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
17    defer cancel()
18
19	r := chiwrap.NewRouter(func(err error) {
20		log.Printf("Router log test: Error occured: %v", err)
21	})
22	r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
23		name := request.URL.Query().Get("name")
24		if name == "" {
25			return httperror.BadRequestProblem("name is required", "Bad User Input").
26                WithType("https://example.com/errors/validation").
27                WithInstance("/api/echo").
28                WithExtension("invalid_field", "name").
29                WithExtension("expected_format", "string").
30                WithExtension("actual_value", name).
31                ToHttpError()
32		}
33
34		writer.Write([]byte("Hello " + name))
35		return nil
36	})
37
38	svr := http.Server{
39		Addr:    ":8080",
40		Handler: r,
41	}
42	go func() {
43		if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
44			log.Fatalf("Failed to start server: %v", err)
45		}
46	}()
47
48    <-ctx.Done()
49    svr.Shutdown(context.Background())
50}

κ²°λ‘ 

Dengan demikian, penanganan kesalahan menggunakan router terpusat dapat mengurangi beban untuk selalu memeriksa kode kesalahan dan menulis pesan kesalahan yang sesuai. Selain itu, dengan menyediakan pesan kesalahan terstruktur menggunakan RFC7807, klien dapat terbantu dalam memahami dan menangani kesalahan. Melalui metode ini, penanganan kesalahan API HTTP yang ditulis dalam bahasa Go dapat dibuat lebih sederhana dan konsisten.

Kode untuk artikel ini dapat ditemukan di repositori gosuda/httpwrap.