Skip to content

Commit

Permalink
Fix Issue #20
Browse files Browse the repository at this point in the history
- Allow manual response in endpoints using underlying Node response
  object
  - Add this.done() to endpoint context to indicate response has been
    handled manually
- Provide clearer error responses when null or undefined is returned
  from endpoint
- Add tests for new functionality
  • Loading branch information
kahmali committed Apr 3, 2015
1 parent 7174a62 commit 3d2c82a
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 20 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## Unreleased

#### Fixed
- Issue #20: Allow [manual response](https://github.com/kahmali/meteor-restivus#thisresponse) in
endpoints using underlying [Node response object](https://nodejs.org/api/http.html#http_class_http_serverresponse).

## [v0.6.2] - 2015-03-04

#### Fixed
Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -926,30 +926,54 @@ In the above examples, all the endpoints except the GETs will require [authentic
Each endpoint has access to:
##### `this.user`
- _Meteor.user_
- The authenticated `Meteor.user`. Only available if `useAuth` and `authRequired` are both `true`.
If not, it will be `undefined`.
##### `this.userId`
- _String_
- The authenticated user's `Meteor.userId`. Only available if `useAuth` and `authRequired` are both
`true`. If not, it will be `undefined`.
##### `this.urlParams`
- _Object_
- Non-optional parameters extracted from the URL. A parameter `id` on the path `posts/:id` would be
available as `this.urlParams.id`.
##### `this.queryParams`
- _Object_
- Optional query parameters from the URL. Given the url `https://yoursite.com/posts?likes=true`,
`this.queryParams.likes => true`.
##### `this.bodyParams`
- _Object_
- Parameters passed in the request body. Given the request body `{ "friend": { "name": "Jack" } }`,
`this.bodyParams.friend.name => "Jack"`.
##### `this.request`
- The [Node request object][node-request]
- [_Node request object_][node-request]
##### `this.response`
- The [Node response object][node-response]
- [_Node response object_][node-response]
- If you handle the response yourself using `this.response.write()` or `this.response.writeHead()`
you **must** call `this.done()`. In addition to preventing the default response (which will throw
an error if you've initiated the response yourself), it will also close the connection using
`this.response.end()`, so you can safely omit that from your endpoint.
##### `this.done()`
- _Function_
- **Must** be called after handling the response manually with `this.response.write()` or
`this.response.writeHead()`. This must be called immediately before returning from an endpoint.
```javascript
Restivus.addRoute('manualResponse', {
get: function () {
console.log('Testing manual response');
this.response.write('This is a manual response');
this.done(); // Must call this immediately before return!
}
});
```
### Response Data
Expand Down
24 changes: 18 additions & 6 deletions lib/route.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,24 @@ class @Route
@_resolveEndpoints()
@_configureEndpoints()

# Append the path to the base API path
fullPath = @api.config.apiPath + @path
# Add the path to our list of existing paths
@api.config.paths.push @path

# Setup endpoints on route using Iron Router
fullPath = @api.config.apiPath + @path
Router.route fullPath,
where: 'server'
action: ->
# Flatten parameters in the URL and request body (and give them better names)
# Add parameters in the URL and request body to the endpoint context
# TODO: Decide whether or not to nullify the copied objects. Makes sense to do it, right?
@urlParams = @params
@queryParams = @params.query
@bodyParams = @request.body

# Add function to endpoint context for indicating a response has been initiated manually
@done = =>
@_responseInitiated = true

# Respond to the requested HTTP method if an endpoint has been provided for it
method = @request.method
if method is 'GET' and self.endpoints.get
Expand All @@ -49,6 +54,16 @@ class @Route
else
responseData = {statusCode: 404, body: {status: "error", message:'API endpoint not found'}}

if responseData is null or responseData is undefined
throw new Error "Cannot return null or undefined from an endpoint: #{method} #{fullPath}"
if @response.headersSent and not @_responseInitiated
throw new Error "Must call this.done() after handling endpoint response manually: #{method} #{fullPath}"

if @_responseInitiated
# Ensure the response is properly completed
@response.end()
return

# Generate and return the http response, handling the different endpoint response types
if responseData.body and (responseData.statusCode or responseData.headers)
responseData.statusCode or= 200
Expand All @@ -57,9 +72,6 @@ class @Route
else
self._respond this, responseData

# Add the path to our list of existing paths
@api.config.paths.push @path


###
Convert all endpoints on the given route into our expected endpoint object if it is a bare function
Expand Down
86 changes: 74 additions & 12 deletions test/api_tests.coffee
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
if Meteor.isServer
Meteor.startup ->

describe 'A Restivus API', ->
describe 'An API', ->
context 'that hasn\'t been configured', ->
it 'should have default settings', (test) ->
test.equal Restivus.config.apiPath, 'api/'
Expand Down Expand Up @@ -90,18 +90,80 @@ if Meteor.isServer
test.equal response.message, 'API endpoint not found'
next()

# describe 'A route', ->
# context 'that has been authenticated', ->
# it 'should have access to this.user and this.userId', (test) ->
describe 'An endpoint', ->

it 'should cause an error when it returns null', (test, next) ->
Restivus.addRoute 'testNullResponse',
get: ->
null

HTTP.get 'http://localhost:3000/api/v1/testNullResponse', (error, result) ->
test.isTrue error
test.equal result.statusCode, 500
next()

it 'should cause an error when it returns undefined', (test, next) ->
Restivus.addRoute 'testUndefinedResponse',
get: ->
undefined

HTTP.get 'http://localhost:3000/api/v1/testUndefinedResponse', (error, result) ->
test.isTrue error
test.equal result.statusCode, 500
next()

it 'should be able to handle it\'s response manually', (test, next) ->
Restivus.addRoute 'testManualResponse',
get: ->
@response.write 'Testing manual response.'
@response.end()
@done()

HTTP.get 'http://localhost:3000/api/v1/testManualResponse', (error, result) ->
response = result.content

test.equal result.statusCode, 200
test.equal response, 'Testing manual response.'
next()

it 'should not have to call this.response.end() when handling the response manually', (test, next) ->
Restivus.addRoute 'testManualResponseNoEnd',
get: ->
@response.write 'Testing this.end()'
@done()

HTTP.get 'http://localhost:3000/api/v1/testManualResponseNoEnd', (error, result) ->
response = result.content

test.isFalse error
test.equal result.statusCode, 200
test.equal response, 'Testing this.end()'
next()

it 'should be able to send it\'s response in chunks', (test, next) ->
Restivus.addRoute 'testChunkedResponse',
get: ->
@response.write 'Testing '
@response.write 'chunked response.'
# @done()

HTTP.get 'http://localhost:3000/api/v1/testChunkedResponse', (error, result) ->
response = result.content

test.equal result.statusCode, 200
test.equal response, 'Testing chunked response.'
next()

it 'should respond with an error if this.done() isn\'t called after response is handled manually', (test, next) ->
Restivus.addRoute 'testManualResponseWithoutDone',
get: ->
undefined

#Tinytest.add 'A route - should be configurable', (test)->
# Restivus.configure
# apiPath: '/api/v1'
# prettyJson: true
# auth:
# token: 'apiKey'
#
# test.equal Restivus.config.apiPath, '/api/v1'
HTTP.get 'http://localhost:3000/api/v1/testManualResponseWithoutDone', (error, result) ->
test.isTrue error
test.equal result.statusCode, 500
next()


# context 'that has been authenticated', ->
# it 'should have access to this.user and this.userId', (test) ->

0 comments on commit 3d2c82a

Please sign in to comment.