Tratarea erorilor intr-un API construit pe Laravel

postat acum 2 săptămâni de Stefanescu Mihai in categorie iNoob

Proiectele care au un API in spate devin din ce in ce mai populare, iar laravel ne ajuta sa le scriem foarte rapid si simplu. Un topic despre care nu se prea discuta este tratarea raspunsurilor de eroare. In multe situatii cine facea parea de client spune ca primeste o eroare generica din care nu intelege nimic. In acest articol mi-am propus sa va ajut sa scrieti cod in care sa tratati erorile cu gratie, cum sa faceti un sistem ce poate fi usor de folosit chiar daca clientul nu are idee ce se intampla in spate.

Structura codului si erori usor de inteles

Pentru un API, un mesaj de eroare explicit poate fi mult mai valoros decat in cazul unui proiect web based. In cazul un proiect web based, citim mesajul si stim unde am gresit si ce trebuie sa facem pentru a indrepta greseala. In cazul API-urilor avem o situatie diferita, pentru ca de obicei un API este folosit de alt software, deci raspunsul trebuie sa fie usor de folosit de catre celalalt software, trebuie sa fim foarte atenti la HTTP Status Codes.

Fiecare request catre un API se intoarce cu un status code, in mod uzual statusurile 200 (sau mai bine zis 2xx) inseamna success.

Cod

Descriere

404

Not Found (pagina nu a fost gasita)

401

Not authorized (nu esti autentificat)

403

Esti autentificat dar nu ai drepturi de a accessa ruta respectiva

400

Bad request (ceva in neregula cu ruta sau parametrii)

422

Unprocessable Entity (eroare de validare)

500

Eroare de server

Atentie mare, daca nu intoarcem in mod explicit un status code, laravel va intoarce unul pentru noi (atentie, desi intoarce automat in status code s-ar putea sa nu corespunda cu cerintele proiectului sau cu standarul).

Desi aceste status code-uri sunt foarte utile, ba chiar necesare in arhitectura unui proiect, tot va trebui sa atasam un raspuns JSON.

{
    "error": "Resource not found"
}

De fapt, ar trebui sa contina chiar mai multe detalii ajutand clientul sa poata trata eroare cat mai bine. Iata un exemplu de raspuns de la API-ul Facebook:

{
  "error": {
    "message": "Error validating access token: Session has expired on Wednesday, 14-Feb-18 18:00:00 PST. The current time is Thursday, 15-Feb-18 13:46:35 PST.",
    "type": "OAuthException",
    "code": 190,
    "error_subcode": 463,
    "fbtrace_id": "H2il2t5bn4e"
  }
}

In mod normal, ce se gaseste pe cheia "error" se afiseaza in browser/aplicatia de mobil astfel incat sa poata fi citit de utilizator.

Schimbare APP_DEBUG=true pe mediul local

In fisierul .env gasim APP_DEBUG caruia ii putem da o valoare bool, true sau false.

Daca o trecem pe true o data cu raspunsul o sa primim mai multe detalii, exact ca in browser cand avem o eroare o sa avem stack trace-ul.

Atentie mare, aceasta optiune NU trebuie lasata activa in productie!

Fallback pentru rute inexistente

Hai sa pornim de la presupunerea ca cineva ne apeleaza API-ul pe o ruta inexistenta, poate a gresit cand a scris URL-ul. In mod normal asa arata raspunsul pe o ruta ce nu exista:

Request URL: http://invata-programare.dev/api/v1/articles
Request Method: GET
Status Code: 404 Not Found
{
    "message": ""
}

Si acest mesaj este destul de ok, este perfect normal sa primeasca un 404. Dar putem veni cu o imbunatatire. Laravel ne pune la dispozitie Route::fallback(), pe care o putem in fisierul routes/api.php si sa se ocupe de toate rutele ce nu exista.

Route::fallback(function(){
    return response()->json([
        'message' => 'Page Not Found. If error persists, contact [email protected]'], 404);
});

Status codeul ramane acelasi, 404, dar acum avem un mesaj mai bun.

Suprascrie 404 ModelNotFoundException

O exceptie des intalnita in Laravel este aceasta, de obicei aruncata de Model::findOrFail($id). Daca lasam aceasta exceptie netratata, API-ul tau va raspunde asa:

{
    "message": "No query results for model [App\\Article] 2",
    "exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
    ...
}

Mesajul este corect, dar pe client nu il intereseaza acest mesaj. Deci, sfatul meu este sa suprascriem aceasta esceptie. Putem face asta din app/Exceptions/Handler.php in metoda render().

public function render($request, Exception $exception)
{
    if ($exception instanceof ModelNotFoundException) {
        return response()->json([
            'error' => 'Entry for '.str_replace('App\\', '', $exception->getModel()).' not found'], 404);
    }


    return parent::render($request, $exception);
}

Putem prinde tot felul de exceptii in aceasta metoda. in cazul de fata returnam acelasi 404, dar cu un mesaj mai frumos.

{
    "error": "Entry for Article not found"
}

Validari cat mai bune

In foarte multe cazuri developerii nu stau sa se gandeasca foarte mult la validari, se limiteaza la required, date, email, etc. In cazul unui API trebuie sa incercam sa prindem cat mai multe probleme inainte sa apara astfel incat sa evitam probleme mai mari. 

Sa presupunem ca avem urmatorul exemplu, avem metoda store() intr-un controller:

public function store(ArticleRequest $request)
{
    $article = Article::create($request->all());
    
    return (new ArticleResource($article))
        ->response()
        ->setStatusCode(201);
}

Fisierul de Request app/Http/Requests/ArticleRequest.php contine urmatoarele reguli:

public function rules()
{
    return [
        'category_id' => 'required|integer|exists:categories,id',
        'content' => 'required'
    ];
}

Daca trimitem acesti 2 parametri fara valori API-ul nostru va intoarce un cod de 422 de forma:

{
    "message": "The given data was invalid.",
    "errors": { 
        "category_id": ["The category id must be an integer.", "The category id field is required."],
        "content": ["The content field is required."]
    }
}

Dupa cum vedeti, avem toate cheile trimise cu erorile respective. Insa, fara aceste validari Laravel ne va intoarce urmatorul mesaj:

{
    "message": "Server Error"
}

Atat! Server error. Fara alte informatii. Deci clientul ce ne foloseste API-ul nu va avea nici cea mai vaga idee ce a mers prost.

Sunt un tanar programator din Bucuresti ce lucreaza in PHP/Mysql (MySqli/PDO), Laravel, CodeIgniter, MySQL, PostgreSQL, Wordpress, HTML5/CSS3, Sass, Photoshop si multe altele.
Google+ Community Facebook Group