Let's Go میان‌افزار › بازیابی پنیک
قبلی · فهرست · بعدی
فصل ۶.۴.

بازیابی پنیک (Panic Recovery)

در این بخش، یک میان‌افزار بازیابی (Recovery Middleware) ایجاد خواهیم کرد که پنیک‌های زمان اجرا (Runtime Panics) را مدیریت می‌کند و آن‌ها را به پاسخ خطای 500 (500 Error Response) تبدیل می‌کند.

در یک برنامه ساده Go، وقتی کد شما پنیک می‌کند منجر به خاتمه فوری برنامه می‌شود.

اما برنامه وب ما کمی پیچیده‌تر است. سرور HTTP گو فرض می‌کند که تأثیر هر پنیک به گوروتین خدمت‌دهنده به درخواست HTTP فعال محدود می‌شود (به یاد داشته باشید، هر درخواست در گوروتین خودش مدیریت می‌شود).

به طور خاص، پس از یک پنیک، سرور ما یک ردیابی پشته را در لاگ خطای سرور ثبت می‌کند (که ما بعداً در کتاب در مورد آن صحبت خواهیم کرد)، پشته را برای گوروتین تحت تأثیر باز می‌کند (با فراخوانی هر تابع معوق در طول مسیر) و اتصال HTTP زیرین را می‌بندد. اما برنامه را متوقف نمی‌کند، بنابراین مهم است که هر پنیک در هندلرهای شما باعث از کار افتادن سرور نمی‌شود.

اما اگر یک پنیک در یکی از هندلرهای ما رخ دهد، کاربر چه چیزی خواهد دید؟

بیایید نگاهی بیندازیم و یک پنیک عمدی در هندلر home خود ایجاد کنیم.

File: cmd/web/handlers.go
package main

...

func (app *application) home(w http.ResponseWriter, r *http.Request) {
    snippets, err := app.snippets.Latest()
    if err != nil {
        app.serverError(w, r, err)
        return
    }

    data := app.newTemplateData(r)
    data.Snippets = snippets

    app.render(w, r, http.StatusOK, "home.tmpl", data)
}

...

برنامه خود را مجدداً راه‌اندازی کنید...

$ go run ./cmd/web
time=2024-03-18T11:29:23.000+00:00 level=INFO msg="starting server" addr=:4000

... و یک درخواست HTTP برای صفحه اصلی از یک پنجره ترمینال دوم ارسال کنید:

$ curl -i http://localhost:4000
curl: (52) Empty reply from server

متأسفانه، به دلیل بسته شدن اتصال HTTP زیرین پس از پنیک توسط Go، تنها چیزی که دریافت می‌کنیم یک پاسخ خالی است.

این تجربه خوبی برای کاربر نیست. ارسال یک پاسخ HTTP مناسب با وضعیت 500 Internal Server Error مناسب‌تر و معنادارتر خواهد بود.

یک روش تمیز برای انجام این کار، ایجاد میان‌افزاری است که پنیک را بازیابی می‌کند و متد کمکی app.serverError() ما را فراخوانی می‌کند. برای انجام این کار، می‌توانیم از این واقعیت استفاده کنیم که توابع معوق همیشه هنگام باز شدن پشته پس از یک پنیک فراخوانی می‌شوند.

فایل middleware.go خود را باز کنید و کد زیر را اضافه کنید:

File: cmd/web/middleware.go
package main

import (
    "fmt"
    "net/http"
)

func (app *application) recoverPanic(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Create a deferred function (which will always be run in the event
        // of a panic as Go unwinds the stack).
        defer func() {
            // Use the builtin recover function to check if there has been a
            // panic or not.
            if err := recover(); err != nil {
                // If there was a panic, set a "Connection: close" header on the
                // response. This acts as a trigger to make Go's HTTP server
                // automatically close the current connection after a response has been
                // sent.
                w.Header().Set("Connection", "close")
                // The value returned by recover() has the type any, so we use
                // fmt.Errorf() to normalize it into an error and log it using our
                // custom Logger type.
                err = fmt.Errorf("%v", err)
                app.logger.Error(err.Error())
                app.serverError(w, r, err)
            }
        }()

        next.ServeHTTP(w, r)
    })
}

دو نکته در مورد این کد وجود دارد که ارزش توضیح دارند:

حالا بیایید این را در فایل routes.go به کار ببریم، به طوری که اولین چیز در زنجیره ما برای اجرا باشد (تا پنیک‌ها را در تمام میان‌افزارها و هندلرهای بعدی پوشش دهد).

File: cmd/web/routes.go
package main

import "net/http"

func (app *application) routes() http.Handler {
    mux := http.NewServeMux()

    fileServer := http.FileServer(http.Dir("./ui/static/"))
    mux.Handle("/static/", http.StripPrefix("/static", fileServer))

    mux.HandleFunc("/", app.home)
    mux.HandleFunc("/snippet/view/", app.snippetView)
    mux.HandleFunc("/snippet/create", app.snippetCreate)

    return app.recoverPanic(app.logRequest(app.commonHeaders(mux)))
}

اگر برنامه را مجدداً راه‌اندازی کنید و اکنون درخواستی برای صفحه اصلی ارسال کنید، باید یک پاسخ 500 Internal Server Error به خوبی شکل گرفته را پس از پنیک ببینید، از جمله هدر Connection: close که در موردش صحبت کردیم.

$ go run ./cmd/web
time=2024-03-18T11:29:23.000+00:00 level=INFO msg="starting server" addr=:4000
$ curl -i http://localhost:4000
HTTP/1.1 500 Internal Server Error
Connection: close
Content-Security-Policy: default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com
Content-Type: text/plain; charset=utf-8
Referrer-Policy: origin-when-cross-origin
Server: Go
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Xss-Protection: 0
Date: Wed, 18 Mar 2024 11:29:23 GMT
Content-Length: 22

Internal Server Error

قبل از ادامه، به هندلر home خود برگردید و پنیک عمدی را از کد حذف کنید.


اطلاعات تکمیلی

بازیابی پنیک در گوروتین‌های پس‌زمینه

مهم است که متوجه شوید میان‌افزار ما فقط پنیک‌هایی را که در همان گوروتینی که میان‌افزار recoverPanic() را اجرا کرده رخ می‌دهند، بازیابی می‌کند.

به عنوان مثال، اگر هندلری دارید که یک گوروتین دیگر راه‌اندازی می‌کند (مثلاً برای انجام برخی پردازش‌های پس‌زمینه)، هر پنیکی که در گوروتین دوم رخ دهد بازیابی نخواهد شد — نه توسط میان‌افزار recoverPanic()... و نه توسط بازیابی پنیک داخلی سرور HTTP گو. آنها باعث خروج برنامه شما و از کار افتادن سرور خواهند شد.

بنابراین، اگر گوروتین‌های اضافی را از درون برنامه وب خود راه‌اندازی می‌کنید و احتمال پنیک وجود دارد، باید مطمئن شوید که هر پنیکی را از درون آنها نیز بازیابی می‌کنید. به عنوان مثال:

func (app *application) myHandler(w http.ResponseWriter, r *http.Request) {
    ...

    // Spin up a new goroutine to do some background processing.
    go func() {
        defer func() {
            if err := recover(); err != nil {
                app.logger.Error(fmt.Sprint(err))
            }
        }()

        doSomeBackgroundProcessing()
    }()

    w.Write([]byte("OK"))
}

واژه‌نامه اصطلاحات فنی

اصطلاح فارسی معادل انگلیسی توضیح
میان‌افزار بازیابی Recovery Middleware میان‌افزاری که پنیک‌ها را مدیریت می‌کند
پنیک‌های زمان اجرا Runtime Panics خطاهای غیرمنتظره در زمان اجرای برنامه
پاسخ خطای 500 500 Error Response پاسخ خطای سرور داخلی
زنجیره میان‌افزار Middleware Chain ترکیب چندین میان‌افزار به صورت پشت سر هم
تابع معوق Deferred Function تابعی که در پایان اجرای بلوک فراخوانی می‌شود
بازیابی Recover تابع داخلی Go برای مدیریت پنیک‌ها
هدر اتصال Connection Header هدر HTTP برای کنترل وضعیت اتصال
ثبت خطا Error Logging ذخیره اطلاعات خطاها در گزارش‌ها
مدیریت خطا Error Handling نحوه برخورد با خطاها در برنامه
خطای سرور Server Error خطایی که در سمت سرور رخ می‌دهد