Browse Source

fix: scraper test returns meaningful errors (fixes #6313)

- Check response.ok and throw with HTTP status when site returns 4xx/5xx
- Throw clear errors for: network failure, selector no match, parse failure
- Controller passes through error message; frontend shows error.error.message
pull/6316/head
irfanfaraaz 2 months ago
parent
commit
3b1c79bb47
  1. 7
      apps/api/src/app/admin/admin.controller.ts
  2. 83
      apps/api/src/services/data-provider/manual/manual.service.ts
  3. 15
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

7
apps/api/src/app/admin/admin.controller.ts

@ -259,7 +259,12 @@ export class AdminController {
} catch (error) {
Logger.error(error, 'AdminController');
throw new HttpException(error.message, StatusCodes.BAD_REQUEST);
const message =
error instanceof Error
? error.message
: 'Scraper test failed. Check the URL, selector, and that the site returns the expected content.';
throw new HttpException(message, StatusCodes.BAD_REQUEST);
}
}

83
apps/api/src/services/data-provider/manual/manual.service.ts

@ -276,24 +276,52 @@ export class ManualService implements DataProviderInterface {
): Promise<number> {
let locale = scraperConfiguration.locale;
const response = await fetch(scraperConfiguration.url, {
headers: scraperConfiguration.headers as HeadersInit,
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
});
let response: Response;
try {
response = await fetch(scraperConfiguration.url, {
headers: scraperConfiguration.headers as HeadersInit,
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
});
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : 'Unknown network error';
throw new Error(
`Request failed: ${message}. Check the URL and that the site is reachable.`
);
}
if (!response.ok) {
throw new Error(
`HTTP ${response.status} (${response.statusText}). The site may be blocking requests, rate-limiting, or returning an error page.`
);
}
let value: string;
if (response.headers.get('content-type')?.includes('application/json')) {
const object = await response.json();
const object: unknown = await response.json();
value = String(
query({
object,
pathExpression: scraperConfiguration.selector
})[0]
);
const matched = query({
object: object as object,
pathExpression: scraperConfiguration.selector
})[0] as unknown;
if (matched === undefined || matched === null) {
throw new Error(
'Selector did not match any value in the response. Check the path expression.'
);
}
if (typeof matched === 'object') {
throw new Error(
'Selector matched an object or array. Expected a string or number value.'
);
}
value = typeof matched === 'string' ? matched : String(matched as number);
} else {
const $ = cheerio.load(await response.text());
@ -305,6 +333,12 @@ export class ManualService implements DataProviderInterface {
value = $(scraperConfiguration.selector).first().text();
if (!value?.trim()) {
throw new Error(
'Selector matched no element on the page. Check the selector and that the page structure matches.'
);
}
const lines = value?.split('\n') ?? [];
const lineWithDigits = lines.find((line) => {
@ -314,13 +348,26 @@ export class ManualService implements DataProviderInterface {
if (lineWithDigits) {
value = lineWithDigits;
}
}
return extractNumberFromString({
locale,
value
});
const parsed = extractNumberFromString({
locale,
value: value ?? ''
});
if (
parsed === undefined ||
(typeof parsed === 'number' && Number.isNaN(parsed))
) {
const preview =
(value ?? '').trim().length > 50
? `${(value ?? '').trim().slice(0, 50)}`
: (value ?? '').trim();
throw new Error(
`Could not extract a number from the selected text. Got: "${preview}"`
);
}
return extractNumberFromString({ locale, value });
return parsed;
}
}

15
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -691,9 +691,20 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
symbol: this.data.symbol
})
.pipe(
catchError(({ error }) => {
catchError((err: HttpErrorResponse) => {
const body: unknown = err.error;
const message: string =
typeof body === 'object' &&
body !== null &&
'message' in body &&
typeof (body as { message: unknown }).message === 'string'
? (body as { message: string }).message
: typeof err.message === 'string'
? err.message
: $localize`Scraper test failed`;
this.notificationService.alert({
message: error?.message,
message,
title: $localize`Error`
});
return EMPTY;

Loading…
Cancel
Save