---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: HTTP Hatalarını Daha Az Zahmetle İşleme + RFC7807
description: Go API hata yönetimi sizi yoruyor mu? RFC7807 ile hataları daha sade ve net bir şekilde yönetebilirsiniz.
language: tr
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Genel Bakış

Go dilinde HTTP API oluştururken en zahmetli kısım hata yönetimidir. Tipik olarak aşağıdaki gibi bir kod bulunur.

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

API sayısı az olduğunda, bu şekilde yazmak pek rahatsızlık vermeyebilir. Ancak API sayısı arttıkça ve iç mantık karmaşıklaştıkça üç nokta rahatsız edici hale gelir.

1. Uygun hata kodunun döndürülmesi
2. Çok sayıda sonuç günlüğünün yazılması
3. Açık hata mesajının iletilmesi

## Ana Bölüm

### Uygun Hata Kodunun Döndürülmesi

Elbette birinci madde, yani uygun hata kodunun döndürülmesi, benim kişisel şikayetim sayılabilir. Deneyimli bir geliştirici uygun kodu bulup her seferinde doğru şekilde yerleştirecektir; ancak ben de dahil olmak üzere henüz tecrübesiz geliştiriciler, mantık karmaşıklaştıkça ve çağrı sayısı arttıkça uygun hata kodlarını tutarlı biçimde kullanmakta zorlanabilir. Bunun için birçok yöntem olabilir; en yaygını, API mantık akışını önceden tasarlayıp uygun hatayı döndürecek şekilde kodu yazmaktır. ~~Bunu yapın~~

Ancak bu yaklaşım, IDE (veya Language Server) yardımından yararlanan insan geliştiriciler için en uygun yöntem gibi görünmemektedir. Ayrıca REST API'nin kendisi, hata kodlarında taşınan anlamdan azami ölçüde yararlanmaya dayandığından, farklı bir yöntem önerilebilir. `StatusCode` ve `Message` değerlerini saklayan, `error` arayüzünü uygulayan `HttpError` adında yeni bir tür oluşturulur. Ardından aşağıdaki gibi yardımcı fonksiyonlar sağlanır.

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

`BadRequest` yardımcı fonksiyonu, `StatusCode` değerini 400, `Message` değerini ise argüman olarak alınan değere ayarlanmış bir `HttpError` döndürecektir. Bunun dışında elbette `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired` gibi yardımcı fonksiyonlar da otomatik tamamlama özelliğiyle aranabilir ve eklenebilir. Bu yöntem, hazırlanmış bir tasarım belgesini her seferinde kontrol etmekten daha hızlı olacak; her seferinde sayısal hata kodunu elle girmekten ise daha güvenilir olacaktır. ~~`http.StatusCode` sabitlerinde zaten hepsi var mı? Sessiz olun~~

### Çok Sayıda Sonuç Günlüğünün Yazılması

Hata oluştuğunda elbette günlük kaydı tutulur. API çağrıldığında ve isteğin başarılı mı yoksa başarısız mı olduğuna dair günlük yazılırken, baştan itibaren tüm olası çıkış noktalarına günlük eklemek yazılacak kod miktarını artırır. Bunun yerine handler'ın kendisini bir kez sarmalayarak günlük yönetimini merkezi hale getirebiliriz.

Aşağıda `chi` router'ını saran bir örnek bulunmaktadır.

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

Router yapısı `chi.Router`'ı içinde tutar; bu nedenle `chi.Router`'ın işlevlerini olduğu gibi kullanabilecek şekilde yapılandırılmıştır. `Get` metoduna bakıldığında, yukarıda önerilen yardımcı fonksiyonların döndürdüğü `HttpError` yapısının döndürülüp döndürülmediği kontrol edilir ve uygun şekilde yanıt verilir; `error` durumunda ise hepsi topluca hata callback fonksiyonuna iletilir. Bu callback, kurucu fonksiyon aracılığıyla alınır.

Aşağıda bu paketi kullanarak yazılmış kod yer almaktadır.

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

Nasıl buldunuz? Yalnızca `HttpError`'ı yardımcı fonksiyonlarla oluşturup döndürdüğünüzde, üst kapsamda uygun hata kodu ve mesajıyla yanıt verilebilir; ayrıca her servis uygulaması için uygun günlüğü tutacak bir callback kaydedilerek işlem yapılabilir. Gerekirse genişleterek `RequestID` gibi bilgilerle ayrıntılı günlükleme de yapılabilir.

### Açık Hata Mesajının İletilmesi

Bu amaçla kullanılabilecek belge olarak RFC7807 bulunmaktadır. RFC7807 genellikle aşağıdaki öğeleri tanımlayarak kullanılır.

- `type`: Hata türünü tanımlayan URI. Genellikle hatayı açıklayan bir belgedir.
- `title`: Hatanın ne olduğunu belirten kısa bir açıklamadır.
- `status`: HTTP Status Code ile aynıdır.
- `detail`: İlgili hata hakkında insanların okuyabileceği ayrıntılı açıklamadır.
- `instance`: Hatanın oluştuğu URI'dir. Örneğin `GET /user/info` isteğinde hata oluştuysa, bu değer `/user/info` olacaktır.
- `extensions`: Hatayı açıklamak için kullanılan, JSON Object biçiminde oluşturulan yardımcı öğedir.
  - Örneğin `BadRequest` durumunda kullanıcının girdisi yer alabilir.
  - Ya da `TooManyRequest` durumunda en son istek zamanı yer alabilir.

Bunu kolayca kullanabilmek için `HttpError` ile aynı konumda bulunan `httperror` paketine yeni bir dosya eklenir, `RFC7807Error` yapısı oluşturulur ve metot zincirleme (method chaining) deseniyle oluşturulabilmesi sağlanır.

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

`Type` alanındaki `"about:blank"` varsayılan değerdir. Bu, mevcut olmayan bir sayfayı ifade eder. Aşağıda hatalı istek için hata oluşturma örneği yer almaktadır.

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

Basit bir metot zincirleme ile kullanıcıya yönelik yapılandırılmış bir hata mesajı oluşturulabilir. Ayrıca yukarıda önce yazdığımız merkezi router'ı kullanabilmek için aşağıdaki metot da desteklenebilir.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// If marshaling fails, fall back to just using the detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Bu metot doğrudan kullanılarak yukarıdaki örnek düzenlendiğinde sonuç şu şekilde olur.

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

## Sonuç

Hataları bu şekilde merkezi bir router ile işlediğinizde, her seferinde hata kodunu kontrol etme ve uygun hata mesajını yazma yükünü azaltabilirsiniz. Ayrıca RFC7807 kullanılarak yapılandırılmış hata mesajları sunulması, istemcinin hatayı anlamasına ve işlemesine yardımcı olabilir. Bu yöntemlerle Go dilinde yazılmış HTTP API'lerinde hata yönetimini daha pratik ve tutarlı hale getirebilirsiniz.

Bu yazının kodlarını [gosuda/httpwrap](https://github.com/gosuda/httpwrap) deposunda inceleyebilirsiniz.
