Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GOT not able to handle Passthrough streams #2111

Closed
vijhhh2 opened this issue Aug 16, 2022 · 16 comments · Fixed by #2120
Closed

GOT not able to handle Passthrough streams #2111

vijhhh2 opened this issue Aug 16, 2022 · 16 comments · Fixed by #2120

Comments

@vijhhh2
Copy link

vijhhh2 commented Aug 16, 2022

Current Behaviour
Hi, I am trying to upload the file stream to another server.
Please check the code below

const fd = new FormData({
    readable: true
});
const stream = getFileStream();
fd.append('file', stream, {
    filename: 'upload.txt',
});
await got.post('http://localhost:3001/upload', {
    body: fd,
});

Hi, I am creating a file stream like the below snippet.

const getFileStream = () => {
    const fileStream = createReadStream(filePath);
    const passThrough = new PassThrough();
    return fileStream.pipe(passThrough);
};

I have to use passthrough for some reasons.

If I do something like the above I get an error
'ERR_GOT_REQUEST_ERROR'

But if I change getFileStream function like below it works basically I am not using Passthrough

const getFileStream = () => {
    const fileStream = createReadStream(filePath);
    const passThrough = new PassThrough();
    return fileStream;
};

If I do the same in Axios and node fetch it's working with passthrough.

Expected Behaviour:
It should upload the file

Node Version
v14.18.2

@szmarczak
Copy link
Collaborator

  1. What's the error message?
  2. What Got version are you using?
  3. What FormData package are you using?

@vijhhh2
Copy link
Author

vijhhh2 commented Aug 17, 2022

RequestError
    at Request._beforeError (file:///Users/vmangapoti/Documents/node/node-got/node_modules/got/dist/source/core/index.js:295:21)
    at Request.flush (file:///Users/vmangapoti/Documents/node/node-got/node_modules/got/dist/source/core/index.js:284:18) {
  input: undefined,
  code: 'ERR_GOT_REQUEST_ERROR',
  timings: undefined,
  options: {
    request: undefined,
    agent: { http: undefined, https: undefined, http2: undefined },
    h2session: undefined,
    decompress: true,
    timeout: {
      connect: undefined,
      lookup: undefined,
      read: undefined,
      request: undefined,
      response: undefined,
      secureConnect: undefined,
      send: undefined,
      socket: undefined
    },
    prefixUrl: '',
    body: FormData {
      _overheadLength: 152,
      _valueLength: 0,
      _valuesToMeasure: [ [PassThrough] ],
      writable: false,
      readable: true,
      dataSize: 0,
      maxDataSize: 2097152,
      pauseStreams: true,
      _released: false,
      _streams: [],
      _currentStream: null,
      _insideLoop: false,
      _pendingNext: false,
      _boundary: '--------------------------779001998119603009640146',
      _events: [Object: null prototype] { error: [Function] },
      _eventsCount: 1
    },
    form: undefined,
    json: undefined,
    cookieJar: undefined,
    ignoreInvalidCookies: false,
    searchParams: undefined,
    dnsLookup: undefined,
    dnsCache: undefined,
    context: {},
    hooks: {
      init: [],
      beforeRequest: [],
      beforeError: [],
      beforeRedirect: [],
      beforeRetry: [],
      afterResponse: []
    },
    followRedirect: true,
    maxRedirects: 10,
    cache: undefined,
    throwHttpErrors: true,
    username: '',
    password: '',
    http2: false,
    allowGetBody: false,
    headers: {
      'user-agent': 'got (https://github.com/sindresorhus/got)',
      'content-type': 'multipart/form-data; boundary=--------------------------779001998119603009640146'
    },
    methodRewriting: false,
    dnsLookupIpVersion: undefined,
    parseJson: [Function: parse],
    stringifyJson: [Function: stringify],
    retry: {
      limit: 2,
      methods: [ 'GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'TRACE' ],
      statusCodes: [
        408, 413, 429, 500,
        502, 503, 504, 521,
        522, 524
      ],
      errorCodes: [
        'ETIMEDOUT',
        'ECONNRESET',
        'EADDRINUSE',
        'ECONNREFUSED',
        'EPIPE',
        'ENOTFOUND',
        'ENETUNREACH',
        'EAI_AGAIN'
      ],
      maxRetryAfter: undefined,
      calculateDelay: [Function: calculateDelay],
      backoffLimit: Infinity,
      noise: 100
    },
    localAddress: undefined,
    method: 'POST',
    createConnection: undefined,
    cacheOptions: {
      shared: undefined,
      cacheHeuristic: undefined,
      immutableMinTimeToLive: undefined,
      ignoreCargoCult: undefined
    },
    https: {
      alpnProtocols: undefined,
      rejectUnauthorized: undefined,
      checkServerIdentity: undefined,
      certificateAuthority: undefined,
      key: undefined,
      certificate: undefined,
      passphrase: undefined,
      pfx: undefined,
      ciphers: undefined,
      honorCipherOrder: undefined,
      minVersion: undefined,
      maxVersion: undefined,
      signatureAlgorithms: undefined,
      tlsSessionLifetime: undefined,
      dhparam: undefined,
      ecdhCurve: undefined,
      certificateRevocationLists: undefined
    },
    encoding: undefined,
    resolveBodyOnly: false,
    isStream: false,
    responseType: 'text',
    url: URL {
      href: 'http://localhost:3001/upload',
      origin: 'http://localhost:3001',
      protocol: 'http:',
      username: '',
      password: '',
      host: 'localhost:3001',
      hostname: 'localhost',
      port: '3001',
      pathname: '/upload',
      search: '',
      searchParams: URLSearchParams {},
      hash: ''
    },
    pagination: {
      transform: [Function: transform],
      paginate: [Function: paginate],
      filter: [Function: filter],
      shouldContinue: [Function: shouldContinue],
      countLimit: Infinity,
      backoff: 0,
      requestLimit: 10000,
      stackAllItems: false
    },
    setHost: true,
    maxHeaderSize: undefined,
    signal: undefined,
    enableUnixSockets: true
  }
}

got version
12.3.1

Form data Library
https://www.npmjs.com/package/form-data

@szmarczak
Copy link
Collaborator

Can you try https://github.com/octet-stream/form-data instead? form-data is outdated and incompatible.

@vijhhh2
Copy link
Author

vijhhh2 commented Aug 17, 2022

Hi @szmarczak
Now working function is not also working with the new Form-Data library.
Below are the two ways I have tried but had no luck.

fd.set("file", {
   type: "text/plain",
    name: "upload.txt",
    [Symbol.toStringTag]: "File",
    stream() {
        return getFileStream();
    }
});
const encoder = new FormDataEncoder(fd);
console.log('Length', encoder.contentLength);

fd.set("file", new BlobFromStream(stream, encoder.contentLength), 'upload.txt');

The above two ways are taken from new fromdata-node library itself
and I am not using passthrough here
Please check the error below.

RequestError: options.body.getLength is not a function
    at Request._beforeError (file:///Users/vmangapoti/Documents/node/node-got/node_modules/got/dist/source/core/index.js:295:21)
    at Request.flush (file:///Users/vmangapoti/Documents/node/node-got/node_modules/got/dist/source/core/index.js:284:18)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at Request._finalizeBody (file:///Users/vmangapoti/Documents/node/node-got/node_modules/got/dist/source/core/index.js:528:26)
    at Request.flush (file:///Users/vmangapoti/Documents/node/node-got/node_modules/got/dist/source/core/index.js:266:24)
    at lastHandler (file:///Users/vmangapoti/Documents/node/node-got/node_modules/got/dist/source/create.js:37:26)
    at iterateHandlers (file:///Users/vmangapoti/Documents/node/node-got/node_modules/got/dist/source/create.js:49:28)
    at got (file:///Users/vmangapoti/Documents/node/node-got/node_modules/got/dist/source/create.js:69:16)
    at Function.got.<computed> [as post] (file:///Users/vmangapoti/Documents/node/node-got/node_modules/got/dist/source/create.js:172:42)
    at file:///Users/vmangapoti/Documents/node/node-got/src/server1.ts:81:19
    at Layer.handle [as handle_request] (/Users/vmangapoti/Documents/node/node-got/node_modules/express/lib/router/layer.js:95:5)
    at next (/Users/vmangapoti/Documents/node/node-got/node_modules/express/lib/router/route.js:144:13)
    at Route.dispatch (/Users/vmangapoti/Documents/node/node-got/node_modules/express/lib/router/route.js:114:3) {
  input: undefined,
  code: 'ERR_GOT_REQUEST_ERROR',
  timings: undefined,
  options: {
    request: undefined,
    agent: { http: undefined, https: undefined, http2: undefined },
    h2session: undefined,
    decompress: true,
    timeout: {
      connect: undefined,
      lookup: undefined,
      read: undefined,
      request: undefined,
      response: undefined,
      secureConnect: undefined,
      send: undefined,
      socket: undefined
    },
    prefixUrl: '',
    body: Readable {
      _readableState: ReadableState {
        objectMode: true,
        highWaterMark: 1,
        buffer: BufferList { head: null, tail: null, length: 0 },
        length: 0,
        pipes: [],
        flowing: null,
        ended: false,
        endEmitted: false,
        reading: false,
        sync: true,
        needReadable: false,
        emittedReadable: false,
        readableListening: false,
        resumeScheduled: false,
        errorEmitted: false,
        emitClose: true,
        autoDestroy: true,
        destroyed: true,
        errored: null,
        closed: true,
        closeEmitted: true,
        defaultEncoding: 'utf8',
        awaitDrainWriters: null,
        multiAwaitDrain: false,
        readingMore: false,
        dataEmitted: false,
        decoder: null,
        encoding: null,
        [Symbol(kPaused)]: null
      },
      _events: [Object: null prototype] { error: [Function] },
      _eventsCount: 1,
      _maxListeners: undefined,
      _read: [Function (anonymous)],
      _destroy: [Function (anonymous)],
      [Symbol(kCapture)]: false
    },
    form: undefined,
    json: undefined,
    cookieJar: undefined,
    ignoreInvalidCookies: false,
    searchParams: undefined,
    dnsLookup: undefined,
    dnsCache: undefined,
    context: {},
    hooks: {
      init: [],
      beforeRequest: [],
      beforeError: [],
      beforeRedirect: [],
      beforeRetry: [],
      afterResponse: []
    },
    followRedirect: true,
    maxRedirects: 10,
    cache: undefined,
    throwHttpErrors: true,
    username: '',
    password: '',
    http2: false,
    allowGetBody: false,
    headers: {
      'user-agent': 'got (https://github.com/sindresorhus/got)',
      'content-type': 'multipart/form-data; boundary=form-data-boundary-lg6pafdfz0p8r6vi',
      'content-length': '43'
    },
    methodRewriting: false,
    dnsLookupIpVersion: undefined,
    parseJson: [Function: parse],
    stringifyJson: [Function: stringify],
    retry: {
      limit: 2,
      methods: [ 'GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'TRACE' ],
      statusCodes: [
        408, 413, 429, 500,
        502, 503, 504, 521,
        522, 524
      ],
      errorCodes: [
        'ETIMEDOUT',
        'ECONNRESET',
        'EADDRINUSE',
        'ECONNREFUSED',
        'EPIPE',
        'ENOTFOUND',
        'ENETUNREACH',
        'EAI_AGAIN'
      ],
      maxRetryAfter: undefined,
      calculateDelay: [Function: calculateDelay],
      backoffLimit: Infinity,
      noise: 100
    },
    localAddress: undefined,
    method: 'POST',
    createConnection: undefined,
    cacheOptions: {
      shared: undefined,
      cacheHeuristic: undefined,
      immutableMinTimeToLive: undefined,
      ignoreCargoCult: undefined
    },
    https: {
      alpnProtocols: undefined,
      rejectUnauthorized: undefined,
      checkServerIdentity: undefined,
      certificateAuthority: undefined,
      key: undefined,
      certificate: undefined,
      passphrase: undefined,
      pfx: undefined,
      ciphers: undefined,
      honorCipherOrder: undefined,
      minVersion: undefined,
      maxVersion: undefined,
      signatureAlgorithms: undefined,
      tlsSessionLifetime: undefined,
      dhparam: undefined,
      ecdhCurve: undefined,
      certificateRevocationLists: undefined
    },
    encoding: undefined,
    resolveBodyOnly: false,
    isStream: false,
    responseType: 'text',
    url: URL {
      href: 'http://localhost:3001/upload',
      origin: 'http://localhost:3001',
      protocol: 'http:',
      username: '',
      password: '',
      host: 'localhost:3001',
      hostname: 'localhost',
      port: '3001',
      pathname: '/upload',
      search: '',
      searchParams: URLSearchParams {},
      hash: ''
    },
    pagination: {
      transform: [Function: transform],
      paginate: [Function: paginate],
      filter: [Function: filter],
      shouldContinue: [Function: shouldContinue],
      countLimit: Infinity,
      backoff: 0,
      requestLimit: 10000,
      stackAllItems: false
    },
    setHost: true,
    maxHeaderSize: undefined,
    signal: undefined,
    enableUnixSockets: true
  }
}

@vijhhh2
Copy link
Author

vijhhh2 commented Aug 17, 2022

After some debugging, I found that
below line from get-body-size file
return promisify(body.getLength.bind(body))();
producing that error

Old FormData library is unable to calculate content length when I am trying to pipe stream with passthrough
it is missing some keys something like fd

@szmarczak
Copy link
Collaborator

Can you please send a full example with the new form-data library?

@vijhhh2
Copy link
Author

vijhhh2 commented Aug 17, 2022

Hi @szmarczak Please find the full example below
One Way

/**
 * App that will upload the file to the server
 */
import express from 'express';
import { createReadStream } from "node:fs";
import { dirname, join, resolve } from 'node:path';
import { PassThrough, pipeline, Readable, Transform } from "node:stream";
import { got } from 'got';
import { fileURLToPath } from 'node:url';
import cors from 'cors';
import { FormData } from 'formdata-node'
import { FormDataEncoder } from "form-data-encoder"
import axios from 'axios';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const app = express();
const filePath = resolve(join(__dirname,'../assets/upload.txt'));
app.use(cors());


const getFileStream = () => {
    const fileStream = createReadStream(filePath);
    const passThrough = new PassThrough();
    // fileStream.pipe(passThrough);
    // return passThrough;
    return fileStream;
};

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.post('/sendFile', async (req, res) => {
    try {
        const fd = new FormData();

        fd.set("file", {
          type: "text/plain",
          name: "upload.txt",
          [Symbol.toStringTag]: "File",
          stream() {
            return getFileStream();
          }
        });
        const encoder = new FormDataEncoder(fd);

        await got.post('http://localhost:3001/upload', {
            body: Readable.from(encoder),
            headers: encoder.headers
        });

        // Below code works with old Formdata library not checked with new Formdata library
        // await axios({
        //     url: 'http://localhost:3001/upload',
        //     method: 'post',
        //     data: fd,
        // });

        res.send('uploaded');
    } catch (error) {
        res.send('uploaded error');
        console.log(error);
    }
});

app.listen(3000, () => {
    console.log('Example app listening on port 3000!');
});

second way

/**
 * App that will upload the file to the server
 */
import express from 'express';
import { createReadStream } from "node:fs";
import { dirname, join, resolve } from 'node:path';
import { PassThrough, pipeline, Readable, Transform } from "node:stream";
import { got } from 'got';
import { fileURLToPath } from 'node:url';
import cors from 'cors';
import { FormData } from 'formdata-node'
import { FormDataEncoder } from "form-data-encoder"
import axios from 'axios';

class BlobFromStream {
    #stream
    size
  
    constructor(stream: any, size: any) {
      this.#stream = stream
      this.size = size
    }
  
    stream() {
      return this.#stream
    }
  
    get [Symbol.toStringTag]() {
      return "Blob"
    }
  }

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const app = express();
const filePath = resolve(join(__dirname,'../assets/upload.txt'));
app.use(cors());

const getFileStream = () => {
    const fileStream = createReadStream(filePath);
    const passThrough = new PassThrough();
    // fileStream.pipe(passThrough);
    // return passThrough;
    return fileStream;
};

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.post('/sendFile', async (req, res) => {
    try {
        const fd = new FormData();
        const stream = getFileStream();

        fd.set("file", {
          type: "text/plain",
          name: "upload.txt",
          [Symbol.toStringTag]: "File",
          stream() {
            return stream;
          }
        });
        const encoder = new FormDataEncoder(fd);

        fd.set("file", new BlobFromStream(stream, encoder.contentLength), 'upload.txt');

        await got.post('http://localhost:3001/upload', {
            body: Readable.from(encoder),
            headers: encoder.headers
        });

        // Below code works with old Formdata library not checked with new Formdata library
        // await axios({
        //     url: 'http://localhost:3001/upload',
        //     method: 'post',
        //     data: fd,
        // });

        res.send('uploaded');
    } catch (error) {
        res.send('uploaded error');
        console.log(error);
    }
});

app.listen(3000, () => {
    console.log('Example app listening on port 3000!');
});

Example node server to upload files

/**
 * App that will upload the file to the server
 */
import express from 'express';
import multer from 'multer';
import cors from 'cors';

const app = express();
app.use(cors());
const upload = multer({ dest: '../uploads/' });

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.post('/upload', upload.any(), (req, res) => {
    console.log(req.files);
    res.send('uploaded');
    setTimeout(() => {
        console.clear();
    }, 10000);
})

app.listen(3001, () => {
    console.log('Example app listening on port 3001!');
    console.clear();
});

@szmarczak
Copy link
Collaborator

The first one definitely should work, I've opened octet-stream/form-data-encoder#9 to track this.

@szmarczak
Copy link
Collaborator

In the second example you're incorrectly passing encoder.contentLength as the stream content length.

@szmarczak
Copy link
Collaborator

@vijhhh2
Copy link
Author

vijhhh2 commented Aug 17, 2022

@szmarczak I tried with the above package. The Process is similar to what formdata-node package does for streams.
I didn't get any error, But I am not receiving any files on the other server.

@octet-stream
Copy link
Contributor

As I mentioned here, you need to avoid Content-Length header (there's FormDataEncoder#contentType property for that) if you have File or Blob entries without known size property. This should be enough to transfer files that way.

@vijhhh2
Copy link
Author

vijhhh2 commented Aug 17, 2022

@octet-stream @szmarczak Removing the Content-Length header fixed the issue.
Now I am able to use passthrough streams.
Below I am pasting the working code so that it will act as a reference for someone facing the same issue

/**
 * App that will upload the stream to the server using got
 */
import express from "express";
import { createReadStream } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { PassThrough, Readable } from "node:stream";
import { got } from "got";
import { fileURLToPath } from "node:url";
import cors from "cors";
import { FormData } from "formdata-node";
import { FormDataEncoder } from "form-data-encoder";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const app = express();
const filePath = resolve(join(__dirname, "../assets/upload.txt"));
app.use(cors());

/**
 * Imitate streams coming from other destinations
 */
const getFileStream = () => {
  const fileStream = createReadStream(filePath);
  const passThrough = new PassThrough();
  fileStream.pipe(passThrough);
  return passThrough;
};

app.post("/sendFile", async (req, res) => {
  try {
    const fd = new FormData();
    
    fd.set("file", {
      type: "text/plain",
      name: "upload.txt",
      [Symbol.toStringTag]: "File",
      stream() {
        return getFileStream();
      },
    });

    const encoder = new FormDataEncoder(fd);

    const headers = {
      "Content-Type": encoder.headers["Content-Type"],
    };
    await got.post("http://localhost:3001/upload", {
      body: Readable.from(encoder),
      headers: headers,
    });

    res.send("uploaded");
  } catch (error) {
    res.send("uploaded error");
    console.log(error);
  }
});

app.listen(3000, () => {
  console.log("Example app listening on port 3000!");
});

Thanks a lot for the help @octet-stream @szmarczak

@vijhhh2 vijhhh2 closed this as completed Aug 17, 2022
@szmarczak szmarczak reopened this Aug 18, 2022
@szmarczak
Copy link
Collaborator

We need to add a test

@octet-stream
Copy link
Contributor

I already tested it in form-data-encoder - it will not return Content-Length if FormData has an entry without known length.

@octet-stream
Copy link
Contributor

Actually, I think I can add a test and send a PR if you want :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants