GoSuda

HTTP Hatalarını Daha Az Zahmetli Şekilde İşlemek + RFC7807

By snowmerak
views ...

Genel Bakış

Go dilinde http api oluştururken, en zahmetli şey hata yönetimidir. Tipik olarak şöyle bir kod bulunur.

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}

Eğer çok fazla API yoksa, bu şekilde yazmak pek bir sıkıntı yaratmayacaktır. Ancak, API sayısı arttıkça ve iç mantık karmaşıklaştıkça üç şey rahatsız edici olmaya başlar.

  1. Uygun hata kodu döndürme
  2. Yazılan çok sayıda sonuç logu
  3. Net hata mesajı gönderme

Ana Bölüm

Uygun hata kodu döndürme

Elbette 1 numara, uygun hata kodu döndürme, benim kişisel bir şikayetimdir. Tecrübeli bir geliştirici uygun kodu bulup her seferinde iyi bir şekilde yerleştirecektir, ancak benim gibi henüz acemi olan geliştiriciler, mantık karmaşıklaştıkça ve çağrı sayısı arttıkça uygun hata kodlarını düzenli olarak kullanmakta zorluk yaşayabilirler. Buna yönelik çeşitli yöntemler olacaktır ve en tipik olanı, önceden API mantık akışını tasarladıktan sonra uygun hatayı döndürecek şekilde kod yazmaktır. Öyle yapın

Ancak bu, IDE (veya Language Server) yardımı alan insan geliştiriciler için en iyi yöntem gibi görünmemektedir. Ayrıca REST API'nin kendisi, hata kodlarında bulunan anlamı azami düzeyde kullandığı için başka bir yöntem önerilebilir. HttpError adında yeni bir error (hata) interface uygulaması oluşturularak StatusCode ve Message'ı depolaması sağlanır. Ve aşağıdaki gibi bir 헬퍼 함수 (yardımcı fonksiyon) sağlanır.

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

BadRequest 헬퍼 함수, StatusCode olarak 400'ü, Message olarak ise argüman olarak aldığı değeri ayarlayan bir HttpError döndürecektir. Bunların dışında elbette NotImplement, ServiceUnavailable, Unauthorized, PaymentRequired gibi 헬퍼 함수ları otomatik tamamlama özelliği ile sorgulayıp eklemek mümkün olacaktır. Bu, hazırlanan tasarım dokümanını her seferinde kontrol etmekten daha hızlı ve hata kodlarını her seferinde sayısal olarak girmekten daha kararlı olacaktır. http.StatusCode sabitlerinde hepsi mi var? Şşşt

Yazılan çok sayıda sonuç logu

Hata oluştuğunda doğal olarak log bırakılır. API çağrıldığında ve isteğin başarılı mı yoksa başarısız mı olduğuna dair log bırakırken, başlangıçtan itibaren tüm beklenen sonlanma noktalarına log bırakmak yazılacak kod sayısını artırır. Bu, 핸들러'ın (işleyici) kendisini bir kez sarmalayarak merkezi olarak yönetilebilir hale gelir.

Aşağıda chi 라우터'ı (yönlendirici) sarmalama örneği verilmiştir.

 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}

라우터 (yönlendirici) 구조체 (yapı), içinde chi.Router bulundurarak chi.Router'ın işlevlerini olduğu gibi kullanacak şekilde yapılandırılmıştır. Get 메서드'ına (metot) bakarsanız, az önce yukarıda önerdiğim 헬퍼 함수'nın (yardımcı fonksiyon) döndürdüğü HttpError 구조체가 döndürüldü mü diye kontrol edildikten sonra uygun şekilde döndürülür ve bir error (hata) durumunda toplu olarak hata 콜백 (geri çağırma) fonksiyonuna iletilir. Bu 콜백, 생성자 (yapıcı) aracılığıyla girdi olarak alınır.

Aşağıda bu 패키지'yi (paket) kullanarak yazılmış bir kod bulunmaktadır.

 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}

Nasıl? Sadece HttpError'ı bir 헬퍼 함수 (yardımcı fonksiyon) olarak oluşturup döndürürseniz, üst 스코프'ta (kapsam) uygun hata kodu ve mesajıyla yanıt döndürülürken, uygulanan her hizmet için uygun log bırakılabilmesi amacıyla bir 콜백 (geri çağırma) kaydedilerek işlem yapılabilir. Ek olarak ihtiyaç duyulursa genişletilerek RequestID gibi şeyler kullanılarak ayrıntılı 로깅 (günlükleme) mümkün olacaktır.

Net hata mesajı gönderme

Bunun için RFC7807 adında bir belge bulunmaktadır. RFC7807 genellikle aşağıdaki öğeleri tanımlayarak kullanılır.

  • type: Hata türünü tanımlayan bir URI. Genellikle hatayı açıklayan bir belgedir.
  • title: Hatanın ne olduğuna dair tek satırlık bir açıklamadır.
  • status: HTTP Status Code ile aynıdır.
  • detail: Söz konusu hataya dair insan tarafından okunabilir ayrıntılı bir açıklamadır.
  • instance: Hatanın oluştuğu URI'dır. Örneğin GET /user/info'da bir hata oluştuysa, /user/info o değer olacaktır.
  • extensions: JSON Object biçiminde 구성되는 (oluşturulan) hatayı açıklamak için ikincil öğelerdir.
    • Örneğin, BadRequest durumunda kullanıcının girdisi dahil edilebilir.
    • Veya TooManyRequest durumunda en son istek zamanı dahil edilebilir.

Bunu kolayca kullanmak için HttpError ile aynı konumda bulunan httperror 패키지'sinde (paket) yeni bir dosya oluşturup RFC7807Error 구조체'sini (yapı) oluşturur ve 메서드 체이닝 패턴iyle (metot zincirleme deseni) oluşturulmasını sağlarız.

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

Type öğesinin "about:blank" değeri varsayılandır. Var olmayan bir sayfayı ifade eder. Aşağıda hatalı bir istek için hata oluşturma örneği verilmiştir.

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

Basit bir 메서드 체이닝 (metot zincirleme) ile kullanıcıya yönelik yapılandırılmış bir hata mesajı oluşturulabilir. Ayrıca yukarıda daha önce yazılan merkezileştirilmiş 라우터'ı (yönlendirici) kullanmak için aşağıdaki 메서드 (metot) desteklenebilir.

1func (p *RFC7807Error) ToHttpError() *HttpError {
2	jsonBytes, err := json.Marshal(p)
3	if err != nil {
4		// Eğer marshaling başarısız olursa, sadece detayı kullanarak geri dönün
5		return New(p.Status, p.Detail)
6	}
7	return New(p.Status, string(jsonBytes))
8}

Bunu olduğu gibi kullanarak yukarıdaki örneği düzenlersek şöyle olur.

 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}

Sonuç

Bu şekilde merkezileştirilmiş bir 라우터 (yönlendirici) kullanarak hatayı yönetmek, her seferinde hata kodunu kontrol etme ve uygun hata mesajını yazma yükünü azaltabilir. Ayrıca RFC7807'yi kullanarak yapılandırılmış hata mesajları sağlamak, istemcinin hatayı anlamasına ve işlemesine yardımcı olabilir. Bu yöntemler aracılığıyla Go dilinde yazılmış HTTP API'larının hata yönetimini daha basit ve tutarlı hale getirmek mümkündür.

Söz konusu yazının kodu gosuda/httpwrap 레포지토리'sinde (depo) incelenebilir.