---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Cara Menangani Error HTTP dengan Lebih Mudah + RFC7807
description: Apakah penanganan error pada Go API terasa merepotkan? Tangani error dengan lebih sederhana dan jelas menggunakan RFC7807.
language: id
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Ikhtisar

Saat membuat HTTP API dengan bahasa Go, bagian yang paling merepotkan adalah penanganan error. Contoh kode yang umum digunakan adalah sebagai berikut.

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

Jika jumlah API hanya sedikit, penulisan seperti ini tidak akan menimbulkan banyak kendala. Namun, seiring bertambahnya jumlah API dan semakin kompleksnya logika internal, tiga hal berikut mulai terasa mengganggu.

1. Pengembalian kode error yang tepat
2. Jumlah penulisan log hasil yang banyak
3. Pengiriman pesan error yang jelas

## Pembahasan

### Pengembalian Kode Error yang Tepat

Tentu saja, poin 1, yaitu pengembalian kode error yang tepat, merupakan keluhan pribadi saya. Pengembang yang berpengalaman akan selalu mencari dan menyisipkan kode yang sesuai setiap kali, tetapi saya sendiri dan pengembang yang masih kurang berpengalaman dapat mengalami kesulitan untuk secara konsisten menulis kode error yang sesuai ketika logika menjadi kompleks dan jumlah pemanggilannya semakin banyak. Terdapat berbagai cara untuk mengatasi hal ini, dan yang paling umum adalah merancang alur logika API terlebih dahulu, lalu menulis kode yang mengembalikan error yang sesuai. ~~Silakan lakukan seperti itu~~

Namun, pendekatan ini tidak tampak sebagai cara yang optimal bagi pengembang manusia yang dibantu oleh IDE (atau Language Server). Selain itu, karena REST API sendiri memanfaatkan makna yang terkandung dalam kode error secara maksimal, kita dapat mengusulkan metode lain. Buatlah implementasi baru dari interface `error`, yaitu `HttpError`, yang menyimpan `StatusCode` dan `Message`. Lalu sediakan fungsi pembantu (helper) seperti berikut.

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

Fungsi pembantu `BadRequest` akan mengembalikan `HttpError` dengan `StatusCode` diatur ke 400 dan `Message` diatur sesuai argumen yang diterima. Selain itu, tentu saja fungsi pembantu seperti `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired`, dan lainnya dapat ditelusuri serta ditambahkan melalui fitur autocomplete. Cara ini lebih cepat dibandingkan memeriksa dokumen desain yang sudah disiapkan setiap kali, dan lebih andal dibandingkan memasukkan kode error berupa angka secara manual setiap saat. ~~Semua sudah ada di konstanta `http.StatusCode`? Ssst~~

### Jumlah Penulisan Log Hasil yang Banyak

Ketika terjadi error, tentu saja kita akan mencatatnya dalam log. Saat mencatat log untuk pemanggilan API, serta untuk menentukan apakah permintaan berhasil atau gagal, menuliskan log pada setiap titik akhir yang mungkin terjadi sejak awal akan memperbanyak jumlah kode yang harus ditulis. Dengan membungkus handler itu sendiri, pengelolaan log dapat dilakukan secara terpusat.

Berikut adalah contoh pembungkus untuk router `chi`.

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

Struktur router memiliki `chi.Router` di dalamnya sehingga fungsionalitas `chi.Router` dapat digunakan secara langsung. Pada metode `Get`, sistem memeriksa apakah error yang dikembalikan adalah struct `HttpError` yang dihasilkan oleh fungsi pembantu yang baru saja diusulkan di atas, lalu meresponsnya dengan tepat. Jika error berupa `error` biasa, maka error tersebut diteruskan secara seragam ke fungsi callback error. Callback ini diterima melalui konstruktor.

Berikut adalah kode yang ditulis dengan memanfaatkan paket ini.

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

Bagaimana menurut Anda? Jika kita hanya membuat `HttpError` sebagai fungsi pembantu dan mengembalikannya, maka pada cakupan yang lebih tinggi kita dapat memberikan respons dengan kode dan pesan error yang sesuai, sekaligus mendaftarkan callback agar setiap layanan dapat mencatat log yang sesuai. Jika diperlukan, sistem ini dapat diperluas dengan menggunakan `RequestID` dan lainnya sehingga pencatatan log yang lebih rinci dimungkinkan.

### Pengiriman Pesan Error yang Jelas

Sebagai dokumen untuk keperluan ini, terdapat RFC7807. RFC7807 umumnya mendefinisikan dan menggunakan elemen-elemen berikut.

- `type`: URI yang mengidentifikasi jenis error. Biasanya berupa dokumen yang menjelaskan error tersebut.
- `title`: Penjelasan singkat dalam satu baris mengenai jenis error yang terjadi.
- `status`: Sama dengan HTTP Status Code.
- `detail`: Penjelasan rinci mengenai error tersebut yang dapat dibaca oleh manusia.
- `instance`: URI tempat terjadinya error. Sebagai contoh, jika error terjadi pada `GET /user/info`, maka nilainya adalah `/user/info`.
- `extensions`: Elemen tambahan untuk menjelaskan error yang disusun dalam bentuk JSON Object.
  - Sebagai contoh, pada kasus `BadRequest`, input pengguna dapat disertakan.
  - Atau pada kasus `TooManyRequest`, waktu permintaan terbaru juga dapat disertakan.

Untuk memudahkan penggunaannya, buatlah berkas baru di dalam paket `httperror`, tempat `HttpError` berada, lalu buat struct `RFC7807Error` dan sediakan cara pembuatannya dengan pola method chaining.

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

Nilai `"about:blank"` pada `Type` adalah nilai default. Nilai tersebut menunjukkan halaman yang tidak ada. Berikut adalah contoh pembuatan error untuk permintaan yang tidak valid.

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

Dengan method chaining yang sederhana, pesan error yang terstruktur untuk pengguna dapat dibuat. Selain itu, untuk memanfaatkan router terpusat yang telah dibuat sebelumnya, kita dapat menyediakan method berikut.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Jika marshaling gagal, gunakan detail saja
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Dengan menggunakan method tersebut secara langsung, contoh di atas dapat diubah menjadi seperti berikut.

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

## Kesimpulan

Dengan menangani error melalui router terpusat seperti ini, beban untuk memeriksa kode error dan menulis pesan error yang sesuai setiap kali dapat dikurangi. Selain itu, dengan memanfaatkan RFC7807 untuk menyediakan pesan error yang terstruktur, klien dapat lebih mudah memahami dan menangani error tersebut. Melalui pendekatan ini, penanganan error pada HTTP API yang ditulis dengan bahasa Go dapat dibuat lebih sederhana dan konsisten.

Kode pada artikel ini dapat dilihat di repositori [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
