diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 8529d908..ea47c411 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -3,8 +3,7 @@ namespace App\Exceptions; use Exception; -use Illuminate\Validation\ValidationException; -use Symfony\Component\HttpKernel\Exception\HttpException; +use Illuminate\Support\Arr; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; class Handler extends ExceptionHandler @@ -13,8 +12,34 @@ class Handler extends ExceptionHandler * A list of the exception types that should not be reported. */ protected $dontReport = [ - HttpException::class, - ValidationException::class, + \Illuminate\Auth\AuthenticationException::class, + \Illuminate\Auth\Access\AuthorizationException::class, + \Symfony\Component\HttpKernel\Exception\HttpException::class, + \Illuminate\Database\Eloquent\ModelNotFoundException::class, + \Illuminate\Validation\ValidationException::class, PrettyPageException::class, ]; + + protected function convertExceptionToArray(Exception $e) + { + return [ + 'message' => $e->getMessage(), + 'exception' => true, + 'trace' => collect($e->getTrace()) + ->map(function ($trace) { + return Arr::only($trace, ['file', 'line']); + }) + ->filter(function ($trace) { + return Arr::has($trace, 'file'); + }) + ->map(function ($trace) { + $trace['file'] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $trace['file']); + return $trace; + }) + ->filter(function ($trace) { + return \Illuminate\Support\Str::startsWith($trace['file'], 'app'); + }) + ->values(), + ]; + } } diff --git a/resources/assets/src/scripts/net.ts b/resources/assets/src/scripts/net.ts index 470bb579..94ce5a46 100644 --- a/resources/assets/src/scripts/net.ts +++ b/resources/assets/src/scripts/net.ts @@ -37,20 +37,28 @@ export async function walkFetch(request: Request): Promise { if (response.ok) { return body } + let message = body.message - // Process validation errors from Laravel. if (response.status === 422) { + // Process validation errors from Laravel. const { errors }: { message: string, errors: { [field: string]: string[] } } = body return { code: 1, message: Object.keys(errors).map(field => errors[field][0])[0], } } else if (response.status === 403) { - showModal(body.message, undefined, 'warning') + showModal(message, undefined, 'warning') return } - throw new HTTPError(body.message || body, cloned) + if (body.exception && Array.isArray(body.trace)) { + const trace = (body.trace as { file: string, line: number }[]) + .map((t, i) => `[${i + 1}] ${t.file}#L${t.line}`) + .join('\n') + message = `${message}\n
${trace}
` + } + + throw new HTTPError(message || body, cloned) } catch (error) { emit('fetchError', error) showAjaxError(error) diff --git a/resources/assets/tests/scripts/net.test.ts b/resources/assets/tests/scripts/net.test.ts index 3873d56b..4510c492 100644 --- a/resources/assets/tests/scripts/net.test.ts +++ b/resources/assets/tests/scripts/net.test.ts @@ -148,6 +148,21 @@ test('process backend errors', async () => { }, clone: () => ({}), }) + .mockResolvedValueOnce({ + status: 500, + headers: new Map([['Content-Type', 'application/json']]), + json() { + return Promise.resolve({ + message: 'fake exception', + exception: true, + trace: [ + { file: 'k.php', line: 2 }, + { file: 'v.php', line: 3 }, + ], + }) + }, + clone: () => ({}), + }) const result: { code: number, @@ -158,6 +173,11 @@ test('process backend errors', async () => { await net.walkFetch({ headers: new Headers() } as Request) expect(showModal).toBeCalledWith('forbidden', undefined, 'warning') + + await net.walkFetch({ headers: new Headers() } as Request) + expect(showAjaxError.mock.calls[0][0].message).toBe( + 'fake exception\n
[1] k.php#L2\n[2] v.php#L3
' + ) }) test('inject to Vue instance', () => { diff --git a/resources/misc/changelogs/en/4.4.0.md b/resources/misc/changelogs/en/4.4.0.md index b9417bff..45864108 100644 --- a/resources/misc/changelogs/en/4.4.0.md +++ b/resources/misc/changelogs/en/4.4.0.md @@ -5,6 +5,7 @@ ## Tweaked - Push notifications to queue for performance. +- Optimized exception stack of Ajax error. ## Fixed diff --git a/resources/misc/changelogs/zh_CN/4.4.0.md b/resources/misc/changelogs/zh_CN/4.4.0.md index c56c1d40..d6531ce3 100644 --- a/resources/misc/changelogs/zh_CN/4.4.0.md +++ b/resources/misc/changelogs/zh_CN/4.4.0.md @@ -5,6 +5,7 @@ ## 调整 - 发送通知时将任务推送到队列 +- 优化 Ajax 中的错误显示 ## 修复 diff --git a/tests/ExceptionsTests/HandlerTest.php b/tests/ExceptionsTests/HandlerTest.php new file mode 100644 index 00000000..8a225a84 --- /dev/null +++ b/tests/ExceptionsTests/HandlerTest.php @@ -0,0 +1,18 @@ +get('/abc', ['Accept' => 'application/json'])->decodeResponseJson(); + $this->assertIsString($json['message']); + $this->assertTrue($json['exception']); + $this->assertTrue(collect($json['trace'])->every(function ($trace) { + return Str::startsWith($trace['file'], 'app') && is_int($trace['line']); + })); + } +}