Hãy làm cho các Errors & Exceptions của bạn thân thiện hơn với người dùng
Như title, thường thì khi bắt tay vào làm một project mới, các dev sẽ quan tâm làm thế nào để nó chạy đầu tiên, sau đó là đến clean code rồi Unit Test các thứ, nhưng dường như có một vấn đề đã bị khá là nhiều dev bỏ quên, đó là xử lý các lỗi và Exception phát sinh khi sản phẩm đã lên ...
Như title, thường thì khi bắt tay vào làm một project mới, các dev sẽ quan tâm làm thế nào để nó chạy đầu tiên, sau đó là đến clean code rồi Unit Test các thứ, nhưng dường như có một vấn đề đã bị khá là nhiều dev bỏ quên, đó là xử lý các lỗi và Exception phát sinh khi sản phẩm đã lên Production. Việc này sẽ dẫn đến 2 vấn đề:
- Người dùng khó chịu và không hiểu ứng dụng đang gặp vấn đề gì, ví dụ như khi click vào 1 button nào đó lại nhận được dòng thông báo to tướng: Undefined Index abc xyz ?? ???? ??
- Dễ để lộ các thông tin về mã nguồn hoặc CSDL, ví dụ như cái error: Cannot Insert into column abc gì đó not null chẳng hạn. Bằng những thứ đơn giản như vậy mà tin tặc có thể sử dụng để tấn công hệ thống
Vậy nên hãy hạn chế những điều này bằng một số cách dưới đây.
1. Đặt APP_DEBUG=false
Mở file .env lên là bạn có thể nhìn thấy cài đặt này ngay. Nếu bạn đặt nó là true thì Laravel sẽ trả về lỗi một cách khá là chi tiết bao gồm cả các class hay DB tables, …
Như đã nói ở trên, việc này gây ra các vấn đề cực kỳ lớn về bảo mật. Rất nhiều người đã quên tắt chế độ này khi đưa project lên production nên để an toàn, hãy tắt luôn nó ngay cả khi bạn đang trong quá trình phát triển, hãy chỉ nên bật khi cần thiết. Lý do của việc này là để giúp bạn suy nghĩ giống như một người dùng bình thường khi chỉ nhận được dòng chữ “Server error” và không có thông tin gì thêm. Nói cách khác, bạn sẽ buộc phải suy nghĩ cách xử lý lỗi và học cách tự mình nghĩ ra các thông báo lỗi phù hợp trong từng trường hợp.
2. Fallback Method
Đây mới là tình huống đầu tiên và cũng là thường gặp nhất, đó là khi ai đó gọi đến một API route không tồn tại, ví dụ người đấy gõ nhầm link chẳng hạn, theo như mặc định thì Laravel sẽ trả về kiểu như này:
1 2 3 4 5 6 7 8 9 |
Request URL<span class="token operator">:</span> http<span class="token operator">:</span>//test/api/v1/get-something Request Method<span class="token operator">:</span> GET Status Code<span class="token operator">:</span> <span class="token number">404</span> Not Found <span class="token punctuation">{</span> <span class="token property">"message"</span><span class="token operator">:</span> <span class="token string">""</span> <span class="token punctuation">}</span> |
Cơ bản thì thế này là được rồi nhưng bạn có thể làm cách khác để giải thích rõ ràng hơn kèm một đoạn message bằng cách sử dụng phương thức Route::fallback() đặt cuối cùng tại routes/api.php, phương thức này sẽ xử lý toàn bộ các routes không đúng:
1 2 3 4 5 6 7 8 |
Route<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">fallback</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">response</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">[</span> <span class="token single-quoted-string string">'message'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token single-quoted-string string">'Page Not Found. Check if you entered an invalid link!'</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">404</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Vẫn là code lỗi 404 thôi nhưng giờ message đã có ý nghĩa hơn nhiều rồi, bằng cách này người dùng sẽ có thêm thông tin cần phải làm gì kế tiếp.
3. Override 404 ModelNotFoundException
Một trong những exception hay gặp phải nhất là có model nào đó not found, thường được throw bởi Model::findOrFail($id). Nếu ta cứ để nguyên si như vậy, một exception sẽ được bắn ra ngoài API như sau:
1 2 3 4 5 6 7 8 |
<span class="token punctuation">{</span> <span class="token property">"message"</span><span class="token operator">:</span> <span class="token string">"No query results for model [App\AbcModel] 2"</span><span class="token punctuation">,</span> <span class="token property">"exception"</span><span class="token operator">:</span> <span class="token string">"Symfony\Component\HttpKernel\Exception\NotFoundHttpException"</span><span class="token punctuation">,</span> ... <span class="token punctuation">}</span> |
Việc này thường gặp ở các bạn newbie khi các bạn không biết rằng method findOrFail() sẽ trả về một exception nếu không tìm được bản ghi phù hợp thay vì trả về rỗng.
Cái này không sai nhưng nó không phù hợp với những người dùng cuối, những người không có hiểu biết về công nghệ, hơn nữa nó còn khiến hệ thống bị mất an toàn bảo mật. Vậy nên hãy nhớ try {} catch() {} đầy đủ với những phương thức như thế này.
Tuy nhiên vấn đề lại nảy sinh ở đây, mình là một người rất lười nên mình sẽ ghét phải sử dụng try catch ở mọi nơi mọi chỗ như vậy, đế lúc debug cũng sẽ rất là khổ, vậy nên lời khuyên là hãy ghi đè nó một lần duy nhất, để từ những lần sử dụng sau nó sẽ cho ra một message có nghĩa như sau:
Mở file app/Exceptions/Handler.php, render():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<span class="token keyword">use</span> <span class="token package">Illuminate<span class="token punctuation"></span>Database<span class="token punctuation"></span>Eloquent<span class="token punctuation"></span>ModelNotFoundException</span><span class="token punctuation">;</span> <span class="token comment">// ...</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">render</span><span class="token punctuation">(</span><span class="token variable">$request</span><span class="token punctuation">,</span> Exception <span class="token variable">$exception</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$exception</span> <span class="token keyword">instanceof</span> <span class="token class-name">ModelNotFoundException</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">response</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">[</span> <span class="token single-quoted-string string">'error'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token single-quoted-string string">'Entry for '</span><span class="token punctuation">.</span><span class="token function">str_replace</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'App'</span><span class="token punctuation">,</span> <span class="token single-quoted-string string">'</span><span class="token punctuation">,</span> <span class="token variable">$exception</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">getModel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token single-quoted-string string">' not found'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">404</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token keyword">parent</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">render</span><span class="token punctuation">(</span><span class="token variable">$request</span><span class="token punctuation">,</span> <span class="token variable">$exception</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Từ lần sau khi sử dụng lại findOrFail() mà không có try catch() thì bạn sẽ nhận được Exception với tin nhắn sau đây:
1 2 3 4 5 6 |
<span class="token punctuation">{</span> <span class="token property">"error"</span><span class="token operator">:</span> <span class="token string">"Entry for AbcModel not found"</span> <span class="token punctuation">}</span> |
4. Bắt chặt hơn trong Validation
Một công cụ mà đến ngay cả mình cùng quên không sử dụng rất nhiều, mặc dù nó rất hữu ích, tránh việc phải if else quá nhiều. Bằng cách sử dụng Validation bạn sẽ tránh được việc project bị crash khi người dùng đưa lên một dữ liệu không được phép hoặc đưa thiếu dữ liệu nào đó.
Ngoài ra việc Validate dữ liệu cũng sẽ giúp trả về những message mà người dùng có thể hiểu được để sửa lại kịp thời, ví dụ tôi có một phương thức store() như thế này: