neekuro 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,9 @@ const dns = require('node:dns').promises;
4
4
  const net = require('node:net');
5
5
  const Canvas = require('canvas');
6
6
  const NekoError = require('../classes/errors/Error.js');
7
+ const { version } = require('../../package.json');
7
8
 
9
+ const USER_AGENT = `neekuro/${version} image-loader`;
8
10
  const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
9
11
  const IMAGE_TIMEOUT_MS = 10_000;
10
12
  const MAX_REDIRECTS = 3;
@@ -25,43 +27,48 @@ async function downloadImage(source, label = 'imagen') {
25
27
  await assertPublicHostname(currentURL.hostname, label);
26
28
  const controller = new AbortController();
27
29
  const timeoutId = setTimeout(() => controller.abort(), IMAGE_TIMEOUT_MS);
28
- let response;
30
+ // El timeout cubre tanto las cabeceras como la lectura completa del cuerpo.
29
31
  try {
30
- response = await fetch(currentURL, {
32
+ const response = await fetch(currentURL, {
31
33
  redirect: 'manual',
32
34
  signal: controller.signal,
33
- headers: { 'User-Agent': 'neekuro/2.1 image-loader' }
35
+ headers: { 'User-Agent': USER_AGENT }
34
36
  });
37
+
38
+ if (response.status >= 300 && response.status < 400) {
39
+ await discardBody(response);
40
+ const location = response.headers.get('location');
41
+ if (!location || redirects === MAX_REDIRECTS) {
42
+ throw new NekoError('ImageDownloadError', `Demasiadas redirecciones al descargar ${label}`);
43
+ }
44
+ currentURL = parseRemoteURL(new URL(location, currentURL).href, label);
45
+ continue;
46
+ }
47
+ if (!response.ok) {
48
+ await discardBody(response);
49
+ throw new NekoError('ImageDownloadError', `No se pudo descargar ${label}: HTTP ${response.status}`);
50
+ }
51
+
52
+ const contentType = response.headers.get('content-type')?.split(';', 1)[0].toLowerCase();
53
+ if (!contentType || !IMAGE_CONTENT_TYPES.has(contentType)) {
54
+ await discardBody(response);
55
+ throw new NekoError('ImageFormatError', `${label} no tiene un formato de imagen compatible`);
56
+ }
57
+ const declaredLength = Number(response.headers.get('content-length'));
58
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_IMAGE_BYTES) {
59
+ await discardBody(response);
60
+ throw new NekoError('ImageSizeError', `${label} supera el límite de 10 MB`);
61
+ }
62
+ return await readLimitedBody(response, label);
35
63
  } catch (error) {
36
- const message = error?.name === 'AbortError'
64
+ if (error instanceof NekoError) throw error;
65
+ const message = controller.signal.aborted
37
66
  ? `La descarga de ${label} excedió ${IMAGE_TIMEOUT_MS} ms`
38
67
  : `No se pudo descargar ${label}: ${error?.message ?? 'error desconocido'}`;
39
68
  throw new NekoError('ImageDownloadError', message);
40
69
  } finally {
41
70
  clearTimeout(timeoutId);
42
71
  }
43
-
44
- if (response.status >= 300 && response.status < 400) {
45
- const location = response.headers.get('location');
46
- if (!location || redirects === MAX_REDIRECTS) {
47
- throw new NekoError('ImageDownloadError', `Demasiadas redirecciones al descargar ${label}`);
48
- }
49
- currentURL = parseRemoteURL(new URL(location, currentURL).href, label);
50
- continue;
51
- }
52
- if (!response.ok) {
53
- throw new NekoError('ImageDownloadError', `No se pudo descargar ${label}: HTTP ${response.status}`);
54
- }
55
-
56
- const contentType = response.headers.get('content-type')?.split(';', 1)[0].toLowerCase();
57
- if (!contentType || !IMAGE_CONTENT_TYPES.has(contentType)) {
58
- throw new NekoError('ImageFormatError', `${label} no tiene un formato de imagen compatible`);
59
- }
60
- const declaredLength = Number(response.headers.get('content-length'));
61
- if (Number.isFinite(declaredLength) && declaredLength > MAX_IMAGE_BYTES) {
62
- throw new NekoError('ImageSizeError', `${label} supera el límite de 10 MB`);
63
- }
64
- return readLimitedBody(response, label);
65
72
  }
66
73
  throw new NekoError('ImageDownloadError', `No se pudo descargar ${label}`);
67
74
  }
@@ -112,6 +119,11 @@ function isPrivateAddress(address) {
112
119
  return true;
113
120
  }
114
121
 
122
+ /** Libera la conexión cuando la respuesta no se va a leer. */
123
+ async function discardBody(response) {
124
+ await response.body?.cancel().catch(() => undefined);
125
+ }
126
+
115
127
  async function readLimitedBody(response, label) {
116
128
  if (!response.body) throw new NekoError('ImageDownloadError', `${label} no contiene datos`);
117
129
  const chunks = [];