HTTP Hatalarını Daha Az Zahmetli Şekilde İşlemek + RFC7807
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.
- Uygun hata kodu döndürme
- Yazılan çok sayıda sonuç logu
- 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ğinGET /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.
- Örneğin,
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.