You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by gl...@apache.org on 2021/01/08 09:15:10 UTC

[couchdb-nano] branch main updated: Propagate axios errors 236 (#246)

This is an automated email from the ASF dual-hosted git repository.

glynnbird pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb-nano.git


The following commit(s) were added to refs/heads/main by this push:
     new 46983a6  Propagate axios errors 236 (#246)
46983a6 is described below

commit 46983a63a95ec67cadafda7045fd8e62b46f63f7
Author: Byron Murgatroyd <by...@gmail.com>
AuthorDate: Fri Jan 8 09:15:02 2021 +0000

    Propagate axios errors 236 (#246)
    
    * Propagate axios errors to stream client
    
    Fixes issue #236.
    
    When calling an *AsStream method a client needs to handle errors
    raised by couch. These errors can include retrieving an attachment
    that doesn't exist or querying a deleted view.
    
    * Removing extra blank line
    
    * Correcting README stream function names
    
    The examples of several stream functions we not named *AsStream.
    
    Co-authored-by: Byron Murgatroyd <by...@omanom.com>
---
 README.md                           | 44 +++++++++++++++++++++++++++----------
 lib/nano.js                         | 34 +++++++++++++++++++++++++++-
 test/attachment.getAsStream.test.js | 16 ++++++++++++++
 3 files changed, 82 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index acac8e8..49e4ffb 100644
--- a/README.md
+++ b/README.md
@@ -355,7 +355,9 @@ nano.db.list().then((body) => {
 Lists all the CouchDB databases as a stream:
 
 ```js
-nano.db.list().pipe(process.stdout);
+nano.db.listAsStream()
+  .on('error', (e) => console.error('error', e))
+  .pipe(process.stdout);
 ```
 
 ### nano.db.compact(name, [designname], [callback])
@@ -623,7 +625,9 @@ alice.list({include_docs: true}).then((body) => {
 List all the docs in the database as a stream.
 
 ```js
-alice.list().pipe(process.stdout)
+alice.listAsStream()
+  .on('error', (e) => console.error('error', e))
+  .pipe(process.stdout)
 ```
 
 ### db.fetch(docnames, [params], [callback])
@@ -881,10 +885,14 @@ Fetch documents from a partition as a stream:
 
 ```js
 // fetch document id/revs from a partition
-nano.db.partitionedListAsStream('canidae').pipe(process.stdout)
+nano.db.partitionedListAsStream('canidae')
+  .on('error', (e) => console.error('error', e))
+  .pipe(process.stdout)
 
 // add document bodies but limit size of response
-nano.db.partitionedListAsStream('canidae', { include_docs: true, limit: 5 }).pipe(process.stdout)
+nano.db.partitionedListAsStream('canidae', { include_docs: true, limit: 5 })
+  .on('error', (e) => console.error('error', e))
+  .pipe(process.stdout)
 ```
 
 ### db.partitionedFind(partitionKey, query, [params])
@@ -902,7 +910,9 @@ Query documents from a partition by supplying a Mango selector as a stream:
 
 ```js
 // find document whose name is 'wolf' in the 'canidae' partition
-db.partitionedFindAsStream('canidae', { 'selector' : { 'name': 'Wolf' }}).pipe(process.stdout)
+db.partitionedFindAsStream('canidae', { 'selector' : { 'name': 'Wolf' }})
+  .on('error', (e) => console.error('error', e))
+  .pipe(process.stdout)
 ```
 
 ### db.partitionedSearch(partitionKey, designName, searchName, params, [callback])
@@ -925,7 +935,9 @@ Search documents from a partition by supplying a Lucene query as a stream:
 const params = {
   q: 'name:\'Wolf\''
 }
-db.partitionedSearchAsStream('canidae', 'search-ddoc', 'search-index', params).pipe(process.stdout)
+db.partitionedSearchAsStream('canidae', 'search-ddoc', 'search-index', params)
+  .on('error', (e) => console.error('error', e))
+  .pipe(process.stdout)
 // { total_rows: ... , bookmark: ..., rows: [ ...] }
 ```
 
@@ -953,7 +965,9 @@ const params = {
   endkey: 'b',
   limit: 1
 }
-db.partitionedView('canidae', 'view-ddoc', 'view-name', params).pipe(process.stdout)
+db.partitionedViewAsStream('canidae', 'view-ddoc', 'view-name', params)
+  .on('error', (e) => console.error('error', e))
+  .pipe(process.stdout)
 // { rows: [ { key: ... , value: [Object] } ] }
 ```
 
@@ -1031,7 +1045,9 @@ alice.attachment.get('rabbit', 'rabbit.png').then((body) => {
 ```js
 const fs = require('fs');
 
-alice.attachment.getAsStream('rabbit', 'rabbit.png').pipe(fs.createWriteStream('rabbit.png'));
+alice.attachment.getAsStream('rabbit', 'rabbit.png')
+  .on('error', (e) => console.error('error', e))
+  .pipe(fs.createWriteStream('rabbit.png'));
 ```
 
 ### db.attachment.destroy(docname, attname, [params], [callback])
@@ -1100,7 +1116,9 @@ alice.view('characters', 'happy_ones', { include_docs: true }).then((body) => {
 Same as `db.view` but returns a stream:
 
 ```js
-alice.view('characters', 'happy_ones', {reduce: false}).pipe(process.stdout);
+alice.viewAsStream('characters', 'happy_ones', {reduce: false})
+  .on('error', (e) => console.error('error', e))
+  .pipe(process.stdout);
 ```
 
 ### db.viewWithList(designname, viewname, listname, [params], [callback])
@@ -1227,7 +1245,9 @@ const q = {
   fields: [ "name", "age", "tags", "url" ],
   limit:50
 };
-alice.findAsStream(q).pipe(process.stdout);
+alice.findAsStream(q)
+  .on('error', (e) => console.error('error', e))
+  .pipe(process.stdout);
 ```
 
 ## using cookie authentication
@@ -1307,7 +1327,9 @@ You can pipe the return values of certain nano functions like other stream. For
 const fs = require('fs');
 const nano = require('nano')('http://127.0.0.1:5984/');
 const alice = nano.use('alice');
-alice.attachment.getAsStream('rabbit', 'picture.png').pipe(fs.createWriteStream('/tmp/rabbit.png'));
+alice.attachment.getAsStream('rabbit', 'picture.png')
+  .on('error', (e) => console.error('error', e))
+  .pipe(fs.createWriteStream('/tmp/rabbit.png'));
 ```
 
 then open `/tmp/rabbit.png` and you will see the rabbit picture.
diff --git a/lib/nano.js b/lib/nano.js
index d4e55e3..0d1c666 100644
--- a/lib/nano.js
+++ b/lib/nano.js
@@ -193,6 +193,33 @@ module.exports = exports = function dbScope (cfg) {
     }
   }
 
+  const streamResponseHandler = function (response, req, stream) {
+    const statusCode = response.status || (response.response && response.response.status) || 500
+    if (response.isAxiosError && response.response) {
+      response = response.response
+    }
+    const message = response.statusText
+
+    const responseHeaders = Object.assign({
+      uri: req.url,
+      statusCode: statusCode
+    }, response.headers)
+
+    const error = new Error(message)
+    error.scope = 'couch'
+    error.statusCode = statusCode
+    error.request = req
+    error.headers = responseHeaders
+    error.errid = 'non_200'
+    error.name = 'Error'
+    error.description = message
+    error.reason = message
+
+    log({ err: 'couch', body: message, headers: responseHeaders })
+
+    stream.emit('error', error)
+  }
+
   function relax (opts, callback) {
     if (typeof opts === 'function') {
       callback = opts
@@ -345,7 +372,12 @@ module.exports = exports = function dbScope (cfg) {
     if (opts.stream) {
       // return the Request object for streaming
       const outStream = new stream.PassThrough()
-      axios(req).then((response) => { response.data.pipe(outStream) })
+      axios(req)
+        .then((response) => {
+          response.data.pipe(outStream)
+        }).catch(e => {
+          streamResponseHandler(e, req, outStream)
+        })
       return outStream
     } else {
       if (typeof callback === 'function') {
diff --git a/test/attachment.getAsStream.test.js b/test/attachment.getAsStream.test.js
index 61c54e7..6d80b3b 100644
--- a/test/attachment.getAsStream.test.js
+++ b/test/attachment.getAsStream.test.js
@@ -41,3 +41,19 @@ test('should be able to get an attachment as a stream - GET /db/id/attname - db.
       })
   })
 })
+
+test('should emit an error when stream attachment does not exist - GET /db/id/attname - db.attachment.getAsStream', () => {
+  // test GET /db/id/attname
+  nock(COUCH_URL)
+    .get('/db/id/notexists.gif')
+    .reply(404, 'Object Not Found', { 'content-type': 'application/json' })
+
+  return new Promise((resolve, reject) => {
+    const db = nano.db.use('db')
+    db.attachment.getAsStream('id', 'notexist.gif')
+      .on('error', (e) => {
+        expect(e.statusCode).toStrictEqual(404)
+        resolve()
+      })
+  })
+})