question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Piping Stream from Firebase Function to Client

See original GitHub issue

Version info

node: v10.13.0

firebase-functions: ^2.1.0

firebase-tools: 6.3.0

Test case

Server (Firebase Function):

app.post('/download', (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'application/pdf',
        'Content-disposition': 'attachment; filename=Report.pdf',
        'Access-Control-Allow-Origin' : '*'
    });

    const options = { format: 'letter' };

    pdf.create(req.body.template, options).toStream((err, stream) => {
        const watchdog = stream.pipe(res);

         watchdog.on('finish', () => {
            console.log('Finish event emitted');
            res.end();
        });
    });
});

Client:

fetch('https://us-central1-[id].cloudfunctions.net/app/download', {
            method: "POST",
            mode: "cors",
            body: JSON.stringify(data)
        })
        .then(response => response.body)
        .then(body => {
            const reader = body.getReader();
            return new ReadableStream({
                start(controller) {
                    return pump();
                    function pump() {
                        return reader.read().then(({ done, value }) => {
                            if (done) {
                                controller.close();
                                return;
                            }
                            controller.enqueue(value);
                            return pump();
                        })
                    }
                }
            })
        })
        .then(stream => new Response(stream))
        .then(response => response.blob())
        .then(blob => URL.createObjectURL(blob))
         .then(url => {
            const link = document.createElement('a');
            link.href = url;
            link.download = "Report.pdf";
            link.click();
        })
        .catch(err => console.log(err));

StackOverflow Question: https://stackoverflow.com/questions/54489268/piping-stream-to-client-express-server-to-firebase-functions-migration

Steps to reproduce

I’m not sure if this is an actual bug or if Firebase Functions does not support piping streams. Make a Fetch call after deployment to run function to reproduce.

Expected behavior

When running this code on my own isolated Node/Express Server, without Firebase, everything works fine - that is, a PDF file containing the HTML sent in the body of the POST request is downloaded.

Actual behavior

When migrating the exact same code to Firebase Functions, I see a CORS error, even though I am both manually setting the header to allow CORS and including the CORS NPM module middleware (setting origin to true in app.use()).

Additionally, every function call is both timing out and getting stuck in an infinite loop to the extent that after only calling the function one time today, I have used up my free quota. The function times out and then seems to get called again, then times out and gets called again, and so on.

Function execution took 6002 ms, finished with status: 'timeout'

Were you able to successfully deploy your functions?

No error messages from firebase deploy.

I hope I’m not breaking any guidelines by posting this issue here. Again, I’m not sure if the problem is arising out of a bug with Firebase Functions, whether Firebase Functions does not support piping streams, or if I’m just doing something wrong. I don’t get the why, number one, the request times out when it takes only a second on my local server - I’m using res.send() in all the required callbacks for errors and the completion of the piping, and number two, the functions keeps getting called as if recursively. The Fetch call is inside an event handler for a button click, so it’s not like I keep making a call.

Thank you.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
eithermonadcommented, Feb 5, 2019

@kevinajian

I’m not actually sure what was causing the issue. I believe it might have had something to do with sending a CORS Header in the response when accessing the URL from the same origin/domain. I didn’t think that would cause an issue.

I ended up deleting the function and creating a new one, and then I rewrote the aforementioned code, and it worked fine.

For reference, here is the code in index.js from Firebase Functions:

const app = express();
const router = express.Router();

router.post('/download', (req, res) => {
        
    const options = { format: 'letter', orientation: 'landscape' };

    res.writeHead(200, {
        'Content-Type': 'application/pdf',
        'Content-disposition': 'attachment; filename=Report.pdf',
    });

    pdf.create(req.body.templateHtml, options).toStream((err, stream) => {
        if (err) return res.send(err);

        const watchdog = stream.pipe(res);

         watchdog.on('finish', () => {
            console.log('Finish event emitted');
            res.end();
        });
    });

});

app.use('/api', router);

exports.api = functions.https.onRequest(app);

This uses the html-pdf NPM Package.

The goal of the function is to take a POST Request containing a body with an HTML string, where the HTML string is the innerHTML of a Handlebars Template after Client-Side Rendering, convert that into a PDF, and then pipe back the reponse to the client for download.

On the client, I create a utility function that returns a promise:

export const getBlobUrlFromPdfStream = (route, headers, body) => {
    return new Promise((resolve, reject) => {
        fetch(route, {
            method: "POST",
            headers,
            body: JSON.stringify(body)
        })
        .then(response => response.body)
        .then(body => {
            const reader = body.getReader();
            return new ReadableStream({
                start(controller) {
                    return pump();
                    function pump() {
                        return reader.read().then(({ done, value }) => {
                            if (done) {
                                controller.close();
                                return;
                            }
                            controller.enqueue(value);
                            return pump();
                        });
                    }
                }
            });
        })
        .then(stream => new Response(stream))
        .then(response => response.blob())
        .then(blob => URL.createObjectURL(blob))
        .then(url => resolve(url))
        .catch(err => reject(`Error occurred while attempting Fetch request: ${err}`));
    });
}

And then I can call that function where neccerssary:

const route = 'https://[project-id].firebaseapp.com/api/download';
        const body = { 
            templateHtml: document.getElementById('element').innerHTML 
        };

        const headers = new Headers();
        headers.append('Content-Type', 'application/json');

        getBlobUrlFromPdfStream(
            route,
            headers,
            body     
        ).then(blobUrl => {     // Create an Anchor Tag to enable download
            const link = document.createElement('a');
            link.href = blobUrl;
            link.download = "Report.pdf";
            link.click();
        }).catch(err => console.log(err));

For reference, my firebase.json file contains the required rewrites - giving me a /api/** route for my Node/Express Server and a /** route for React-Router to handle on the client:

"hosting": {
    "public": "public",
    "rewrites": [{
      "source": "/api/**",
      "function": "api"
    }, {
      "source": "**",
      "destination": "/index.html"
    }],
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
0reactions
rtesslercommented, Mar 28, 2020

I did manage to get pdf.create(html, options).toStream((err, stream) to work from a firebase function with options = {format: ‘A4’, base: ‘file:///’ + __dirname + ‘/’} and stream.pipe(res) without a res.end(). Without this I would get invalid attachment messages in abobe reader.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Firebase function Node.js transform stream - Stack Overflow
However, when I try to pipe the query stream through my transform stream, the Firebase Function crashes with the following error message: ...
Read more >
Call functions via HTTP requests | Cloud Functions for Firebase
You can trigger a function through an HTTP request by using functions.https . This allows you to invoke a synchronous function through the...
Read more >
Help. Piping Firebase stream to Express response result in ... - Reddit
I am trying to stream a video (mp4) from firebase storage to <video> on client. What I'm doing is using createReadStream and piping...
Read more >
The Node.js Runtime | Cloud Functions Documentation
The Node.js runtimes use an execution environment based on Ubuntu 18.04. See Cloud Functions execution environment for more information. The library that ...
Read more >
[Solved]-Firebase function Node.js transform stream-node.js
One alternative is to write your response to an object in Cloud Storage, then send a link or reference to that file to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found