Browse Source

Unwrap ABP error envelope from Blob body and validationErrors-only payloads in RestService

pull/25639/head
maliming 4 months ago
parent
commit
49c4ff5a05
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 60
      npm/ng-packs/packages/core/src/lib/services/rest.service.ts
  2. 75
      npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts

60
npm/ng-packs/packages/core/src/lib/services/rest.service.ts

@ -1,7 +1,7 @@
import { HttpClient, HttpHeaders, HttpParameterCodec, HttpParams, HttpRequest } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Observable, from, of, throwError } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
import { ExternalHttpClient } from '../clients/http.client';
import { ABP } from '../models/common';
import { Rest } from '../models/rest';
@ -57,10 +57,12 @@ export class RestService {
} as any)
.pipe(
catchError(err => {
if (!skipHandleError) {
this.tryUnwrapJsonErrorBody(err, effectiveResponseType);
if (skipHandleError) {
return throwError(() => err);
}
return skipHandleError ? throwError(() => err) : this.handleError(err);
return this.normalizeErrorBody(err, effectiveResponseType).pipe(
switchMap(normalizedErr => this.handleError(normalizedErr)),
);
}),
);
}
@ -97,12 +99,46 @@ export class RestService {
}
}
private tryUnwrapJsonErrorBody(err: any, responseType: Rest.ResponseType): void {
if (responseType === Rest.ResponseType.JSON || !err) return;
if (typeof err.error !== 'string' || err.error.length === 0) return;
private normalizeErrorBody(err: any, responseType: Rest.ResponseType): Observable<any> {
if (!err || responseType === Rest.ResponseType.JSON) {
return of(err);
}
if (typeof err.error === 'string') {
this.tryParseJsonErrorText(err, err.error);
return of(err);
}
if (typeof Blob !== 'undefined' && err.error instanceof Blob) {
return from(this.readBlobAsText(err.error)).pipe(
map((text: string) => {
this.tryParseJsonErrorText(err, text);
return err;
}),
catchError(() => of(err)),
);
}
return of(err);
}
private readBlobAsText(blob: Blob): Promise<string> {
if (typeof (blob as any).text === 'function') {
return (blob as any).text();
}
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
reader.onerror = () => reject(reader.error);
reader.readAsText(blob);
});
}
private tryParseJsonErrorText(err: any, text: string): void {
if (!text || text.length === 0) return;
let parsed: any;
try {
parsed = JSON.parse(err.error);
parsed = JSON.parse(text);
} catch {
return;
}
@ -115,7 +151,11 @@ export class RestService {
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
const inner = parsed.error;
if (!inner || typeof inner !== 'object') return false;
return typeof inner.code === 'string' || typeof inner.message === 'string';
return (
typeof inner.code === 'string' ||
typeof inner.message === 'string' ||
Array.isArray(inner.validationErrors)
);
}
private getParams(params: Rest.Params, encoder?: HttpParameterCodec): HttpParams {

75
npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts

@ -278,6 +278,34 @@ describe('HttpClient testing', () => {
await completion;
});
test('should unwrap ABP envelope that carries only validationErrors (no message / code)', async () => {
const completion = new Promise<void>((resolve, reject) => {
spectator.service
.request({ method: HttpMethod.GET, url: '/text' }, { responseType: Rest.ResponseType.Text })
.pipe(
catchError(err => {
try {
expect(typeof err.error).toBe('object');
expect(err.error.error.validationErrors).toHaveLength(1);
resolve();
} catch (e) {
reject(e);
}
return of(null);
}),
)
.subscribe();
});
const req = spectator.expectOne(api + '/text', HttpMethod.GET);
req.flush(
'{"error":{"validationErrors":[{"message":"Required","members":["Email"]}]}}',
{ status: 400, statusText: 'Bad Request' },
);
await completion;
});
test('should leave non-ABP-envelope JSON body alone in text mode', async () => {
const completion = new Promise<void>((resolve, reject) => {
spectator.service
@ -285,7 +313,6 @@ describe('HttpClient testing', () => {
.pipe(
catchError(err => {
try {
// JSON parses fine but no error.code/error.message → keep raw string
expect(typeof err.error).toBe('string');
expect(err.error).toBe('{"foo":"bar"}');
resolve();
@ -340,6 +367,51 @@ describe('HttpClient testing', () => {
await completion;
});
test('should unwrap ABP error envelope from a Blob body in blob mode', async () => {
// HttpTestingController doesn't deliver Blob in HttpErrorResponse; call the helper directly.
const err: any = {
error: new Blob(
['{"error":{"code":"AbpAuthorization.002","message":"forbidden-blob"}}'],
{ type: 'application/json' },
),
};
const normalized: any = await (spectator.service as any)
.normalizeErrorBody(err, Rest.ResponseType.Blob)
.toPromise();
expect(normalized.error).toEqual({
error: { code: 'AbpAuthorization.002', message: 'forbidden-blob' },
});
});
test('should leave non-JSON Blob body alone in blob mode', async () => {
const blob = new Blob([new Uint8Array([0xff, 0xd8, 0xff])], { type: 'image/jpeg' });
const err: any = { error: blob };
const normalized: any = await (spectator.service as any)
.normalizeErrorBody(err, Rest.ResponseType.Blob)
.toPromise();
expect(normalized.error).toBe(blob);
});
test('should swallow Blob.text() rejection and keep the original error in blob mode', async () => {
const fakeBlob = {
text: () => Promise.reject(new Error('boom')),
} as unknown as Blob;
const err: any = { error: fakeBlob, message: 'original' };
Object.setPrototypeOf(fakeBlob, Blob.prototype);
const normalized: any = await (spectator.service as any)
.normalizeErrorBody(err, Rest.ResponseType.Blob)
.toPromise();
expect(normalized).toBe(err);
expect(normalized.error).toBe(fakeBlob);
});
test('should leave non-JSON error body alone in text mode', async () => {
const spy = vi.spyOn(httpErrorReporter, 'reportError');
@ -400,7 +472,6 @@ describe('HttpClient testing', () => {
catchError(() => {
try {
const errArg: any = spy.mock.calls[0][0];
// Angular HttpClient already parsed JSON in json mode → err.error is object
expect(errArg.error).toEqual({ error: { code: 'X', message: 'y' } });
resolve();
} catch (e) {

Loading…
Cancel
Save