How to store request body in GridFS using Restify server? - node.js

Cannot save file larger than pipe chunk size (~64K).
Using mongodb 3.4.0, Relevant node dependencies
restify 4.2.0
mongodb ^2.2.12
lodash 4.16.6
bookeeppingData = {request, id, ...meta}
clone = lodash.cloneDeep(bookkeepingData)
const {
request: req,
id: _id,
meta: metadata,
} = clone
const bucket = new mongodb.GridFSBucket(
db,
{bucketName: 'my_gridfs_collection'}
);
const uploadSteam = bucket.openUploadStreamWithId(
_id,
undefined,
{metadata}
);
req.on('data', (chunk) => {
console.log(`Received ${chunk.length} bytes of data.`);
});
req.on('end', () => {
console.log('There will be no more data.');
});
req.on('error', (e) => {
console.log('req on error', e);
});
uploadSteam.on('finish', function() {
console.log('finsish');
keepThebooks(bookkeepingData);
});
uploadSteam.on('error', (e) => {
console.log('uploadstream on error', e);
});
req.pipe(uploadSteam);
}
When sending a file smaller than the ~64K, The console output is
Received 57259 bytes of data.
There will be no more data.
finish
This is the corresponding curl --verbose output (minus the json response):
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8060 (#0)
* Server auth using Basic with user 'driver'
> POST /artifact?branch=xyz&role=PHOTO&where.lat=55&where.long=77.2&where.acc=12&sys=system&id=1234&sys=system2&id=1234b&created=2018-11-01T18:41:50.850Z&tz=-600 HTTP/1.1
> Host: localhost:8060
> Authorization: Basic xxxxxxxxx
> User-Agent: curl/7.61.1
> Accept: */*
> Content-Type: image/png
> Content-Length: 57259
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 200 OK
< Cache-Control: no-cache, no-store, must-revalidate
< Content-Type: application/json
< Content-Length: 388
< Date: Wed, 23 Jan 2019 20:58:16 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
When sending a file larger than ~64K, the console output is:
Received 65536 bytes of data.
(That's it -- no error)
The corresponding curl --verbose output is:
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8060 (#0)
* Server auth using Basic with user 'driver'
> POST /artifact?branch=xyz&role=PHOTO&where.lat=55&where.long=77.2&where.acc=12&sys=system&id=1234&sys=system2&id=1234b&created=2018-11-01T18:41:50.850Z&tz=-600 HTTP/1.1
> Host: localhost:8060
> Authorization: Basic xxxxxxxxx
> User-Agent: curl/7.61.1
> Accept: */*
> Content-Type: image/png
> Content-Length: 84801
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
* Empty reply from server
* Connection #0 to host localhost left intact
curl: (52) Empty reply from server
I expected this to work, and for the console to be something like:
Received 65536 bytes of data.
Received 19274 bytes of data.
There will be no more data.
finish

I am on the same team.
Turns out we were deep cloning the object containing the stream. When we stopped cloning the entire file uploads fine.

Related

Node.js does not pipe contents

I have a simple Web server, which should send a file. I took the code from another answer.
#! /usr/bin/node
const FS = require ('fs');
const HTTP = require ('http');
const server = HTTP.createServer ();
server.on ('request', (request, response) => {
switch (request.url) {
case '/':
switch (request.method) {
case 'GET':
console.log ("GET /");
let stat = FS.statSync ('index.html');
console.log (stat.size);
response.writeHead (200, { 'Content-Type': 'text/html',
'Content-Lenght': stat.size });
let index = FS.createReadStream ('index.html', 'UTF-8');
index.pipe (response);
response.end ();
return;
}
break;
}
response.writeHead (400, {});
response.end ();
});
server.listen (8080);
When I try to send a GET request with curl, I get no content. My server reports, that the index.html file has 324 bytes:
$ ./server.js
GET /
324
But curl does not show the content. There header contains the content length, but the body is missing.
$ curl -v --noproxy \* http://localhost:8080/
[...]
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html
< Content-Lenght: 324
< Date: Sat, 21 Nov 2020 19:24:31 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
I looks as if the connection got closed before the file has been piped. Is the the error and how can I avoid it?
Remove the response.end ();. You're prematurely closing the response BEFORE the .pipe() gets to do its work (it's asynchronous so it finishes over time and returns before it's done).
In the default configuration, .pipe() will end your response for you when it's done.
You will also notice that the other answer you took this idea from did not have a response.end().

node express send function not working with binary data

I have a GET route in express that should return a binary png image stored in mongodb. However, when I enter the url into chrome to test, the image is downloaded but the request never completes. From the Network tab in Chrome DevTools the request is just stuck in the 'pending' state. I'm only getting this problem with binary data it seems. I have plenty of other json GET requests that work just fine with send().
I am using the send() function like this:
exports.getProjectPng = (req, res) => {
Project.findById(req.params.projectId).select('project.png')
.then(project => {
res.send(project.png.buffer);
});
If I simply replace send() with end() the request completes as expected. Also, perhaps significantly, the png image is actually rendered within the browser rather than downloading as a file.
So why does end() work but send() doesn't?
If you point curl at an express server and see what the response looks like for both methods it is quite interesting. The main difference is what when we call send, the Content-Type header is populated, which is consistent with the Express docs:
When the parameter is a Buffer object, the method sets the Content-Type response header field to “application/octet-stream”, unless previously defined
It's worthwhile noting that res.send() actually calls res.end() internally at the end of the call, so the different behaviour is likely down to something that res.send does in addition to res.end.
It might be worth populating the Content-Type Header in your example to "image/png" before sending.
e.g.
res.set('Content-Type', 'image/png');
For .end():
* Connected to localhost (::1) port 8081 (#0)
> GET /downloadpng_end HTTP/1.1
> User-Agent: curl/7.30.0
> Host: localhost:8081
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Date: Mon, 25 Jun 2018 13:14:58 GMT
< Connection: keep-alive
< Content-Length: 69040
<
And for send():
* Connected to localhost (::1) port 8081 (#0)
> GET /downloadpng_send HTTP/1.1
> User-Agent: curl/7.30.0
> Host: localhost:8081
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/octet-stream
< Content-Length: 69040
< ETag: W/"10db0-KwFSGG5Ib/DQNZChAbluTiKSP0o"
< Date: Mon, 25 Jun 2018 13:15:25 GMT
< Connection: keep-alive
<
Nodejs does not handle straight binary data very well.So , thats what buffer is used to handle binary data.
End() Method
end − This event is fired when there is no more data to read. while send has no guarantee whether its completed or not. You can read more about Buffer on offical docs here

node-libcurl obtaining wrong IP address on POST

I'm new to nodejs so this may be operator error, but I'm trying to make a POST request to PayPay's test URL. I tried using http request in nodejs, but got an odd response. I was able to get curl on the command line to work, so I figured I'd use curl in node.
When I run the following curl command in bash it works:
curl -v --data "USER=myLogin&VENDOR=myLogin&PARTNER=PayPal&PWD=QQQQQQQQQ&CREATESECURETOKEN=Y&SECURETOKENID=fb7810e9-252b-4613-a53b-6432148bfd97&TRXTYPE=S&AMT=100.00" https://pilot-payflowpro.paypal.com
I get the following results (I know the secure token was used, but its a sign the curl call worked):
* Rebuilt URL to: https://pilot-payflowpro.paypal.com/
* Trying 173.0.82.163...
* Connected to pilot-payflowpro.paypal.com (173.0.82.163) port 443 (#0)
* found 173 certificates in /etc/ssl/certs/ca-certificates.crt
* found 704 certificates in /etc/ssl/certs
* ALPN, offering http/1.1
* SSL connection using TLS1.2 / RSA_AES_256_CBC_SHA256
* server certificate verification OK
* server certificate status verification SKIPPED
* common name: pilot-payflowpro.paypal.com (matched)
* server certificate expiration date OK
* server certificate activation date OK
* certificate public key: RSA
* certificate version: #3* subject: C=US,ST=California,L=San Jose,O=PayPal\, Inc.,OU=Payflow Production,CN=pilot-payflowpro.paypal.com
* start date: Wed, 08 Feb 2017 00:00:00 GMT
* expire date: Thu, 28 Feb 2019 23:59:59 GMT
* issuer: C=US,O=Symantec Corporation,OU=Symantec Trust Network,CN=Symantec Class 3 Secure Server CA - G4
* compression: NULL
* ALPN, server did not agree to a protocol
> POST / HTTP/1.1
> Host: pilot-payflowpro.paypal.com
> User-Agent: curl/7.47.0
> Accept: */*
> Content-Length: 179
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 179 out of 179 bytes
< HTTP/1.1 200 OK
< Connection: close
< Server: VPS-3.033.00
< Date: Sat, 13 May 2017 20:13:38 GMT
< Content-type: text/namevalue
< Content-length: 121
<
* Closing connection 0
RESULT=7&RESPMSG=Field format error: Secure Token Id already beenused&SECURETOKENID=fb7810e9-252b-4613-a53b-6432148bfd97e
I then use the following nodejs code (in a method exposed through Express as a GET):
var newSId = uuid.v4();
var curl = new Curl();
var url = 'https://pilot-payflowpro.payapl.com/';
var data = {
'USER': 'myLogin',
'VENDOR': 'myLogin',
'PARTNER': 'PayPal',
'PWD': 'QQQQQQQQQQQ',
'CREATESECURETOKEN': 'Y',
'SECURETOKENID': newSId,
'TRXTYPE': 'S',
'AMT': '100.00'
};
data = qstr.stringify(data);
console.log("Data = " + data);
curl.setOpt(Curl.option.URL, url);
curl.setOpt(Curl.option.POSTFIELDS, data);
curl.setOpt(Curl.option.POST, 1);
curl.setOpt(Curl.option.VERBOSE, true);
curl.perform();
curl.on('end', function(statusCode, body){
this.close();
});
curl.on('error', curl.close.bind(curl));
When I execute the GET via a browser I get the following information dumped to console:
Data = USER=myLogin&VENDOR=myLogin&PARTNER=PayPal&PWD=QQQQQQQQ&CREATESECURETOKEN=Y&SECURETOKENID=4b8f0763-2d09-4c2a-a57e-f5a4dc5be744&TRXTYPE=S&AMT=100.00
* Trying 209.15.13.134...
* connect to 209.15.13.134 port 443 failed: Connection refused
* Failed to connect to pilot-payflowpro.payapl.com port 443: Connection refused
* Closing connection 0
For a reason I can't explain, in the node-libcurl call, a different IP address is being used. The curl command line call uses: 173.0.82.163, but node-libcurl uses: 209.15.13.134. I'm curious why. If there is a way I should have debugged this better, please let me know.
Thanks in advance!
It appears that at least the domain in the URL has a typo, it should be paypal.com and not payapl.com (which is a scammy/spammy site).

Hapi - reply JSON only

How can I adjust the Hapi reply function such that it would reply JSON objects only?
Should I send it as plain and send? I seem not to find a good example
Here is some edit - added some sample code to understand what's happening.
The route:
server.route({
method: 'GET',
path: '/user/',
handler: function (request, reply) {
var ids = null;
mysqlConnection.query('SELECT ID FROM Users;',function(err,rows,fields){
if(err) throw err;
ids = rows;
// console.log(ids);
reply(ids);
});
}
});
The reply:
<html><head></head><body>
<pre style="word-wrap: break-word; white-space: pre-wrap;">[{"ID":1},{"ID":2},{"ID":3},{"ID":4},{"ID":5},{"ID":6},{"ID":7},{"ID":8},{"ID":9},{"ID":10},{"ID":11},{"ID":12},{"ID":13},{"ID":14},{"ID":15},{"ID":16},{"ID":17},{"ID":18},{"ID":19},{"ID":20},{"ID":21}]
</pre></body></html>
I hope I understand the question ok. Are we talking version 8.x ? For me it seems the default. With this code as a route handler,
folders: {
handler: function( request, reply ) {
'use strict';
reply({
folders: folders
}).code( 200 );
}
},
and doing
curl http://localhost:3001/folders
I get the following output
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3001 (#0)
> GET /folders HTTP/1.1
> User-Agent: curl/7.37.1
> Host: localhost:3001
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: application/json; charset=utf-8
< cache-control: no-cache
< content-length: 266
< accept-ranges: bytes
< Date: Tue, 03 Feb 2015 23:19:31 GMT
< Connection: keep-alive
<
{folders ..... }
Also, note that I only call reply()not return reply()
HTH
As for v17 and above, the reply() interface was removed. Now the handlers uses async functions, and you can just return the value.
From the hapi docs example:
// Before
const handler = function (request, reply) {
return reply('ok');
};
// After
const handler = function (request, h) {
return 'ok';
};
Using hapi's reply(data) and passing a data object will do the work for you. Internally, hapi will create the appropriate JSON of your data object and respond it.
There's a tutorial on how to reply JSON for a given request using hapi that might give some more insights.
Using v17 and above, simply returning a naked string doesn't result in a json encoded reply.
Use return JSON.stringify() to ensure the string is json encoded
e.g.
function (request, h) {
return JSON.stringify('ok');
};

Handling Accept headers in node.js restify

I am trying to properly handle Accept headers in RESTful API in node.js/restify by using WrongAcceptError as follows.
var restify = require('restify')
; server = restify.createServer()
// Write some content as JSON together with appropriate HTTP headers.
function respond(status,response,contentType,content)
{ var json = JSON.stringify(content)
; response.writeHead(status,
{ 'Content-Type': contentType
, 'Content-Encoding': 'UTF-8'
, 'Content-Length': Buffer.byteLength(json,'utf-8')
})
; response.write(json)
; response.end()
}
server.get('/api',function(request,response,next)
{ var contentType = "application/vnd.me.org.api+json"
; var properContentType = request.accepts(contentType)
; if (properContentType!=contentType)
{ return next(new restify.WrongAcceptError("Only provides "+contentType)) }
respond(200,response,contentType,
{ "uri": "http://me.org/api"
, "users": "/users"
, "teams": "/teams"
})
; return next()
});
server.listen(8080, function(){});
which works fine if the client provides the right Accept header, or no header as seen here:
$ curl -is http://localhost:8080/api
HTTP/1.1 200 OK
Content-Type: application/vnd.me.org.api+json
Content-Encoding: UTF-8
Content-Length: 61
Date: Tue, 02 Apr 2013 10:19:45 GMT
Connection: keep-alive
{"uri":"http://me.org/api","users":"/users","teams":"/teams"}
The problem is that if the client do indeed provide a wrong Accept header, the server will not send the error message:
$ curl -is http://localhost:8080/api -H 'Accept: application/vnd.me.org.users+json'
HTTP/1.1 500 Internal Server Error
Date: Tue, 02 Apr 2013 10:27:23 GMT
Connection: keep-alive
Transfer-Encoding: chunked
because the client is not assumed to understand the error message, which is in JSON, as
seen by this:
$ curl -is http://localhost:8080/api -H 'Accept: application/json'
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
Content-Length: 80
Date: Tue, 02 Apr 2013 10:30:28 GMT
Connection: keep-alive
{"code":"WrongAccept","message":"Only provides application/vnd.me.org.api+json"}
My question is therefore, how do I force restify to send back the right error status code and body, or am I doing things wrong?
The problem is actually that you're returning a JSON object with a content-type (application/vnd.me.org.api+json) that Restify doesn't know (and therefore, creates an error no formatter found).
You need to tell Restify how your responses should be formatted:
server = restify.createServer({
formatters : {
'*/*' : function(req, res, body) { // 'catch-all' formatter
if (body instanceof Error) { // see text
body = JSON.stringify({
code : body.body.code,
message : body.body.message
});
};
return body;
}
}
});
The body instanceof Error is also required, because it has to be converted to JSON before it can be sent back to the client.
The */* construction creates a 'catch-all' formatter, which is used for all mime-types that Restify can't handle itself (that list is application/javascript, application/json, text/plain and application/octet-stream). I can imagine that for certain cases the catch-all formatter could pose issues, but that depends on your exact setup.

Resources