node.jserror-handlingpromisefetcharcgis-server

Fetching images with node - Error: read ECONNRESET at TLSWrap.onStreamRead


I am fetching and processing images with nodejs, using node-fetch and canvas. So far, things have been working well. I have a series of image urls, and I fetch them all in parallel using Promise.all:

import { loadImage } from 'canvas';

await Promise.all<CanvasImageSource>(
  urls.map((url: string) => loadImage(url)) // <--- return array of promises
)
  .then((images: CanvasImageSource[]): void => {
     // do some stuff with the images
  })
  .catch((e) => { throw e });

This has been working great. But last night I tried a certain image source url that I want to use, and I'm getting the following error:

Error: read ECONNRESET
    at TLSWrap.onStreamRead (internal/stream_base_commons.js:205:27)
---------------------------------------------
    at TLSSocket.Readable.on (_stream_readable.js:838:35)
    at tickOnSocket (_http_client.js:696:10)
    at onSocketNT (_http_client.js:747:5)
    at processTicksAndRejections (internal/process/task_queues.js:84:21)
---------------------------------------------
    at ClientRequest.onSocket (_http_client.js:735:11)
    at setRequestSocket (_http_agent.js:396:7)
    at handleSocketCreation_Inner (_http_agent.js:389:7)
    at oncreate (_http_agent.js:262:5)
    at Agent.createSocket (_http_agent.js:267:5)
    at Agent.addRequest (_http_agent.js:224:10)
    at new ClientRequest (_http_client.js:296:16)
    at Object.request (https.js:314:10)
    at simpleGet ../../etc

I read How do I debug error ECONNRESET in Node.js? , and the answer there seems to suggest that this is an error from the server side. However, when I print the urls I'm passing to loadImage and then access them in the browser, I am able to get an image back just fine, in the browser. One such url is

https://apps.fs.usda.gov/arcx/rest/services/RDW_Wildfire/ProbabilisticWildfireRisk/MapServer/export?bbox=-13795354.864908611%2C6095394.383573096%2C-13785570.92528811%2C6085610.443952593&size=256%2C256&format=png32&bboxSR=102100&imageSR=102100&f=image&layers=show%3A0

which is a GIS raster image service from the US forestry department. Going that url is my browser returns the image no problem.

I thought that perhaps I might be blasting their server with too many requests at once, as the array of urls usually has 6-10 image urls in it for any given call of the function that runs this code, but I reduced the number of urls to 1 so as to only make 1 request, but no change. Still an error. One thing I noticed when accessing these urls in the browser is that the response is a bit slow. Might that have something to do with it?

A similar (but not the same) government image service url works just fine with this code, retrieving many images in parallel without problem. A sample url for one that works is:

https://landfire.cr.usgs.gov/arcgis/rest/services/Landfire/US_200/MapServer/export?bbox=-13814922.744149616%2C6095394.383573096%2C-13805138.804529114%2C6085610.443952593&size=256%2C256&format=png32&bboxSR=102100&imageSR=102100&f=image&layers=show%3A25

(Edit: the landfire servers are down right now, so that url won't work until they're back up)

I tried using longjohn, as suggested in the other question, to get a more verbose printout of the error, but import 'longjohn' in my code seems to change nothing.

Why would these new urls be throwing this error in node, but not in the browser? I know there are other questions with this subject matter, but they don't seem to helpd me debug my specific issue.

Going further - using the answer

@DipakC wrote a great answer that utilizes axios to fetch images. Using his downloadImage, I am able to fetch images as part of a Promise.all, like so:

await Promise.all(
  tilenames.map((tilename: string) => {
    const url = `${baseurl}/${tilename}.png` // baseurl is remote esri url
    return downloadImage({ url });
  })
);

Ultimately, I need these images to be converted into ImageData so their pixel data can be used. This is easily achieved using some canvas methods. Currently, I use the above code, and then reuse my original code, but this time, instead of using the remote urls for the images, I just use the pathname to the images which I've just downloaded:

// immediately after the Promise.all above:

await Promise.all(
  tilenames.map((tilename: string) => {
    const url = `${localpath}/${tilename}.png` // localpath is where image was downloaded to
    return loadImage(url);
  })
)
  .then((images: Image[]) => {
    const canvas: Canvas = createCanvas(256, 256);
    const ctx: RenderingContext = canvas.getContext('2d');
    ctx.drawImage(image, 0, 0, 256, 256);
    const imageData = ctx.getImageData(0, 0, 256, 256);
    // do something with imageData, more processing
  });

Question: Is there any way to avoid this 2 stage process? Meaning, instead of using downloadImage to download the image as a file, can I write its image data directly to a variable, as I'm doing in the second step with loadImage?

Addendum:

This is the source code for 'loadImage' from the node-canvas src code

function loadImage (src) {
  return new Promise((resolve, reject) => {
    const image = new Image()

    function cleanup () {
      image.onload = null
      image.onerror = null
    }

    image.onload = () => { cleanup(); resolve(image) }
    image.onerror = (err) => { cleanup(); reject(err) }

    image.src = src
  })
}

Solution

  • After reviewed your question, I did some R&D for the source URL which you have added the following is the possible solution to fetch images from the below URL.

    Solution 1:

    The below code is completely working fine, with the endless requests. I have reviewed the actual usda.gov site and review the headers and responses. I have used the Axios and verify that here the response received in-stream. I might not sure about canvas whether it will handle the stream response or not but, Axios works fine.

    The reason to use Axios is that I have also tried with canvas somehow it failed to download an image. After studying the actual application and payload and tried with Axios which works fine and found most compatible.

    const axios = require("axios");
    const fs = require("fs");
    const path = require("path");
    
    /**
     * @description create image directory if not exists
     * @param {String} fPath
     * @returns
     */
    async function createDirIfNotExists(fPath) {
      return new Promise((resolve, reject) => {
        if (fs.existsSync(fPath)) {
          console.error("Directory has already created");
          resolve();
        } else {
          fs.mkdirSync(fPath);
          resolve();
        }
      });
    }
    
    /**
     * @description download the image
     * @param {Object} requestBody
     * @returns
     */
    async function downloadImage(requestBody) {
      const fPath = path.resolve(__dirname, "images");
      await createDirIfNotExists(fPath);
      const writer = fs.createWriteStream(`${fPath}/code.png`);
      const streamResponse = await axios(requestBody).catch((err) => {
        console.error(err);
      });
      streamResponse.data.pipe(writer);
      return new Promise((resolve, reject) => {
        writer.on("finish", resolve("success"));
        writer.on("error", reject("error"));
      });
    }
    
    const requestBody = {
      method: "get",
      url: "https://apps.fs.usda.gov/arcx/rest/services/RDW_Wildfire/ProbabilisticWildfireRisk/MapServer/export",
      headers: {},
      params: {
        bbox: "-13795354.864908611,6095394.383573096,-13785570.92528811,6085610.443952593",
        bboxSR: 102100,
        layers: "",
        layerDefs: "",
        size: "256,256",
        imageSR: 102100,
        historicMoment: "",
        format: "png",
        transparent: false,
        dpi: "",
        time: "",
        layerTimeOptions: "",
        dynamicLayers: "",
        gdbVersion: "",
        mapScale: "",
        rotation: "",
        datumTransformations: "",
        layerParameterValues: "",
        mapRangeValues: "",
        layerRangeValues: "",
        f: "image",
      },
      responseType: "stream",
    };
    
    (async () => {
      const response = await downloadImage(requestBody).catch(() => {
        console.error("Something went wrong while downloading file");
        return false;
      });
      if (response === "success") {
        console.info("File downloaded succesfully");
      }
    })();
    

    Hope my answer will help you to resolve your query. But if you still face the same issue let me know. This code is tested in the local machine as well as repl.it.

    ====================================================================

    Solution 2:

    After reviewing your updated description I have also updated my solution and put more efficient solutions in the answer as solution 2.

    Solution 2 read the image streamed buffer and convert it into Uint8ClampedArray which might similar format which returns while use ctx.getImageData() function. I have also change responseType to arraybuffer

    Hence, this solution resolves both problems listed in the question that you don't need to download the image as well as you received image data Uint8ClampedArray format.

    const axios = require("axios");
    const fs = require("fs");
    
    /**
     * @description create image directory if not exists
     * @param {String} fPath
     * @returns
     */
    async function createDirIfNotExists(fPath) {
      return new Promise((resolve, reject) => {
        if (fs.existsSync(fPath)) {
          console.error("Directory has already created");
          resolve();
        } else {
          fs.mkdirSync(fPath);
          resolve();
        }
      });
    }
    
    /**
     * @description get the image data directly from buffer.
     * @param {Object} requestBody
     * @returns
     */
    async function getImageData(requestBody) {
      try {
        const { format } = requestBody.params;
        const arrFormat = {
          png: "image/png",
          jpg: "image/jpeg",
          jpeg: "image/jpeg",
          gif: "image/gif",
        };
        const streamResponse = await axios(requestBody).catch((err) => {
          console.error(err);
        });
        // store the streamed data
        const bufferData = streamResponse.data;
        // convert into in base64 format
        const base64Data = `data:${arrFormat[format]};base64,${Buffer.from(
          bufferData
        ).toString("base64")}`;
        // convert into in Uint8ClampedArray (as I referred from the code ctx.getImageData() return Uint8ClampedArray)
        const imageData = new Uint8ClampedArray(bufferData);
        return {
          base64Data,
          imageData,
        };
      } catch (err) {
        console.error(err);
      }
    }
    
    const requestBody = {
      method: "get",
      url: "https://apps.fs.usda.gov/arcx/rest/services/RDW_Wildfire/ProbabilisticWildfireRisk/MapServer/export",
      headers: {},
      params: {
        bbox: "-13795354.864908611,6095394.383573096,-13785570.92528811,6085610.443952593",
        bboxSR: 102100,
        layers: "",
        layerDefs: "",
        size: "256,256",
        imageSR: 102100,
        historicMoment: "",
        format: "png",
        transparent: false,
        dpi: "",
        time: "",
        layerTimeOptions: "",
        dynamicLayers: "",
        gdbVersion: "",
        mapScale: "",
        rotation: "",
        datumTransformations: "",
        layerParameterValues: "",
        mapRangeValues: "",
        layerRangeValues: "",
        f: "image",
      },
      // responseType: "stream",
      responseType: "arraybuffer",
    };
    
    (async () => {
      const response = await getImageData(requestBody).catch(() => {
        console.error("Something went wrong while downloading file");
        return false;
      });
      console.info("image data received", response);
    })();
    

    Hope this solution 2 might resolve your issue.

    I suggest using queuing when you want to apply complex image manipulation or process. This might be an efficient way to use your memory and CPU.

    Let me know still face an issue.StackOverflow

    I open to update my answer if the StackOverflow community has an optimized or alternate solution.