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

Automatic OPTIONS responses were removed #284

Closed
williamkapke opened this Issue Jan 7, 2013 · 35 comments

Comments

Projects
None yet
@williamkapke

williamkapke commented Jan 7, 2013

A recent checkin (d1366f2) stopped Restify's auto handling of OPTIONS requests. We now have the ability to create them manually.

Although more control is good, manually adding OPTIONS for every endpoint is cumbersome and (if your application relies on CORS requests) can make it hard to move to Restify v2.

I propose that the auto generated OPTIONS responses are "on by default" and developers can override the default by specifying server.opts(...). It would probably also be good to have a way to globally disable them.

@ricardograca

This comment has been minimized.

Show comment
Hide comment
@ricardograca

ricardograca Jan 7, 2013

You don't have to add a handler for OPTIONS on every endpoint. You have two options. The first one is to use the built-in plugin that restores the behavior you're looking for:

server.use(restify.fullResponse())

The other (which I prefer since it affords more control) is to listen for the "MethodNotAllowed" event from the server like so:

// This is a simplified example just to give you an idea
// You will probably need more allowed headers

function unknownMethodHandler(req, res) {
  if (req.method.toLowerCase() === 'options') {
    var allowHeaders = ['Accept', 'Accept-Version', 'Content-Type', 'Api-Version'];

    if (res.methods.indexOf('OPTIONS') === -1) res.methods.push('OPTIONS');

    res.header('Access-Control-Allow-Credentials', true);
    res.header('Access-Control-Allow-Headers', allowHeaders.join(', '));
    res.header('Access-Control-Allow-Methods', res.methods.join(', '));
    res.header('Access-Control-Allow-Origin', req.headers.origin);

    return res.send(204);
  }
  else
    return res.send(new restify.MethodNotAllowedError());
}

server.on('MethodNotAllowed', unknownMethodHandler);

ricardograca commented Jan 7, 2013

You don't have to add a handler for OPTIONS on every endpoint. You have two options. The first one is to use the built-in plugin that restores the behavior you're looking for:

server.use(restify.fullResponse())

The other (which I prefer since it affords more control) is to listen for the "MethodNotAllowed" event from the server like so:

// This is a simplified example just to give you an idea
// You will probably need more allowed headers

function unknownMethodHandler(req, res) {
  if (req.method.toLowerCase() === 'options') {
    var allowHeaders = ['Accept', 'Accept-Version', 'Content-Type', 'Api-Version'];

    if (res.methods.indexOf('OPTIONS') === -1) res.methods.push('OPTIONS');

    res.header('Access-Control-Allow-Credentials', true);
    res.header('Access-Control-Allow-Headers', allowHeaders.join(', '));
    res.header('Access-Control-Allow-Methods', res.methods.join(', '));
    res.header('Access-Control-Allow-Origin', req.headers.origin);

    return res.send(204);
  }
  else
    return res.send(new restify.MethodNotAllowedError());
}

server.on('MethodNotAllowed', unknownMethodHandler);
@williamkapke

This comment has been minimized.

Show comment
Hide comment
@williamkapke

williamkapke Jan 7, 2013

I should have mentioned there are line comments in d1366f2 that give more background on this.

williamkapke commented Jan 7, 2013

I should have mentioned there are line comments in d1366f2 that give more background on this.

@ricardograca

This comment has been minimized.

Show comment
Hide comment
@ricardograca

ricardograca Jan 7, 2013

Ok, but the behavior of version 1.4 in regards to OPTIONS requests was very similar to the second option I posted. In fact I think I copied it from that version. Does it make so much difference to you that you have to define the unknown method handler function yourself to handle OPTIONS requests? If an endpoint does have a handler for OPTIONS this function will be overridden, so I think that covers all cases right?

ricardograca commented Jan 7, 2013

Ok, but the behavior of version 1.4 in regards to OPTIONS requests was very similar to the second option I posted. In fact I think I copied it from that version. Does it make so much difference to you that you have to define the unknown method handler function yourself to handle OPTIONS requests? If an endpoint does have a handler for OPTIONS this function will be overridden, so I think that covers all cases right?

@williamkapke

This comment has been minimized.

Show comment
Hide comment
@williamkapke

williamkapke Jan 7, 2013

Nope, it doesn't make a difference to me.

Yup, seems to handle the cases. Which is why I suggested it in the line comment.

Basically this issue was added to bring awareness and because @mcavage is considering baking it back in & asked if I'd make an issue so this topic was tracked. If he decides to go the unknownMethodHandler route- he'll let the world know on this thread and close the issue. :)

williamkapke commented Jan 7, 2013

Nope, it doesn't make a difference to me.

Yup, seems to handle the cases. Which is why I suggested it in the line comment.

Basically this issue was added to bring awareness and because @mcavage is considering baking it back in & asked if I'd make an issue so this topic was tracked. If he decides to go the unknownMethodHandler route- he'll let the world know on this thread and close the issue. :)

@mcavage

This comment has been minimized.

Show comment
Hide comment
@mcavage

mcavage Jan 8, 2013

Contributor

Yeah - this has already come up enough times in issues and offline emails to me that I'm going to make it "first class". I'm not sure yet what I want it to look like, but I'm probably going to have something like: server.use(restify.cors()), or some such thing.

Contributor

mcavage commented Jan 8, 2013

Yeah - this has already come up enough times in issues and offline emails to me that I'm going to make it "first class". I'm not sure yet what I want it to look like, but I'm probably going to have something like: server.use(restify.cors()), or some such thing.

@mcavage

This comment has been minimized.

Show comment
Hide comment
@mcavage

mcavage Jan 27, 2013

Contributor

Ok - I dropped support back into #master - I would actually like some feedback from you all on whether this is what you want, before I ship it out via NPM. Here's a (stupidly simple) sample, and semantics are explained below:

var restify = require('restify');

function foo(req, res, next) {
        res.send(204);
        next();
}

var srv = restify.createServer();
srv.use(restify.CORS());

srv.put('/foo', foo);
srv.get('/foo', foo);
srv.del('/foo', foo);

srv.listen(8080);

So, to get "simple/actual" request headers for CORS flying, you'll need to use server.use(restify.CORS()) - and this will only take effect for requests that have an origin header. The defaults are pretty sane (I think), which is to set Access-Control-Allow-Origin to * not set Access-Control-Allow-Credentials and to default Access-Control-Expose-Headers to the stuff restify relies on (mainly, versioning). You can override these with at plugin creation time:

server.use(restify.CORS({
    origins: ['foo.com', 'a.foo.com', 'bar.com', ...],
    headers: ['x-ping', 'x-other', ...]
}));

For preflight requests, the internals of restify automatically handle this, similar to the way it did in 1.4, with the exception you can override on a per-URL basis with server.opts, in which case the restify logic won't kick in, and it's on you to do the right thing for CORS.

For the example above, here's some sample curl request showing what happens:

GET request with no Origin header (so no CORS):

$ curl -isS http://127.0.0.1:8080/foo
HTTP/1.1 204 No Content
Date: Sat, 26 Jan 2013 18:45:24 GMT
Connection: keep-alive

Tack on Origin:

$ curl -isS http://127.0.0.1:8080/foo -H 'origin: foo.com'
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: api-version, content-length, content-md5, content-type, date, request-id, response-time
Date: Sat, 26 Jan 2013 18:47:27 GMT
Connection: keep-alive

Here's a bad preflight request (note no access-control-request-method), so it 404s:

$ curl -isS http://127.0.0.1:8080/foo -H 'origin: foo.com' -X OPTIONS
HTTP/1.1 405 Method Not Allowed
Allow: PUT, GET, DELETE, POST
Content-Type: application/json
Content-Length: 67
Date: Sat, 26 Jan 2013 18:48:06 GMT
Connection: keep-alive

{"code":"MethodNotAllowedError","message":"OPTIONS is not allowed"}

And a proper preflight request:

$ curl -isS http://127.0.0.1:8080/foo -H 'origin: foo.com' -X OPTIONS -H 'access-control-request-method: GET'
HTTP/1.1 200 OK
Allow: PUT, GET, DELETE, POST
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: PUT, GET, DELETE, POST
Access-Control-Allow-Headers: accept-version, content-type, request-id, x-api-version, x-request-id
Access-Control-Max-Age: 3600
Date: Sat, 26 Jan 2013 18:49:00 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Play around some, and let me know how that gets on.

Contributor

mcavage commented Jan 27, 2013

Ok - I dropped support back into #master - I would actually like some feedback from you all on whether this is what you want, before I ship it out via NPM. Here's a (stupidly simple) sample, and semantics are explained below:

var restify = require('restify');

function foo(req, res, next) {
        res.send(204);
        next();
}

var srv = restify.createServer();
srv.use(restify.CORS());

srv.put('/foo', foo);
srv.get('/foo', foo);
srv.del('/foo', foo);

srv.listen(8080);

So, to get "simple/actual" request headers for CORS flying, you'll need to use server.use(restify.CORS()) - and this will only take effect for requests that have an origin header. The defaults are pretty sane (I think), which is to set Access-Control-Allow-Origin to * not set Access-Control-Allow-Credentials and to default Access-Control-Expose-Headers to the stuff restify relies on (mainly, versioning). You can override these with at plugin creation time:

server.use(restify.CORS({
    origins: ['foo.com', 'a.foo.com', 'bar.com', ...],
    headers: ['x-ping', 'x-other', ...]
}));

For preflight requests, the internals of restify automatically handle this, similar to the way it did in 1.4, with the exception you can override on a per-URL basis with server.opts, in which case the restify logic won't kick in, and it's on you to do the right thing for CORS.

For the example above, here's some sample curl request showing what happens:

GET request with no Origin header (so no CORS):

$ curl -isS http://127.0.0.1:8080/foo
HTTP/1.1 204 No Content
Date: Sat, 26 Jan 2013 18:45:24 GMT
Connection: keep-alive

Tack on Origin:

$ curl -isS http://127.0.0.1:8080/foo -H 'origin: foo.com'
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: api-version, content-length, content-md5, content-type, date, request-id, response-time
Date: Sat, 26 Jan 2013 18:47:27 GMT
Connection: keep-alive

Here's a bad preflight request (note no access-control-request-method), so it 404s:

$ curl -isS http://127.0.0.1:8080/foo -H 'origin: foo.com' -X OPTIONS
HTTP/1.1 405 Method Not Allowed
Allow: PUT, GET, DELETE, POST
Content-Type: application/json
Content-Length: 67
Date: Sat, 26 Jan 2013 18:48:06 GMT
Connection: keep-alive

{"code":"MethodNotAllowedError","message":"OPTIONS is not allowed"}

And a proper preflight request:

$ curl -isS http://127.0.0.1:8080/foo -H 'origin: foo.com' -X OPTIONS -H 'access-control-request-method: GET'
HTTP/1.1 200 OK
Allow: PUT, GET, DELETE, POST
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: PUT, GET, DELETE, POST
Access-Control-Allow-Headers: accept-version, content-type, request-id, x-api-version, x-request-id
Access-Control-Max-Age: 3600
Date: Sat, 26 Jan 2013 18:49:00 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Play around some, and let me know how that gets on.

@benjie

This comment has been minimized.

Show comment
Hide comment
@benjie

benjie Jan 28, 2013

Adding headers to the restify.CORS({headers:[...]}) option does not add them to ALLOW_HEADERS, thus they are rejected by this loop.

benjie commented Jan 28, 2013

Adding headers to the restify.CORS({headers:[...]}) option does not add them to ALLOW_HEADERS, thus they are rejected by this loop.

@benjie

This comment has been minimized.

Show comment
Hide comment
@benjie

benjie Jan 28, 2013

I think var headers = req.headers['access-control-request-headers']; is a string, so (headers || []).forEach( fails when you request headers - I've changed it to (headers || "").toLowerCase().split(", ").forEach( in my own code, which has stopped it producing errors just there. I would have submitted a pull request, but I'm so new to this code I'm not really sure what form or intent is preferred by the authors.

benjie commented Jan 28, 2013

I think var headers = req.headers['access-control-request-headers']; is a string, so (headers || []).forEach( fails when you request headers - I've changed it to (headers || "").toLowerCase().split(", ").forEach( in my own code, which has stopped it producing errors just there. I would have submitted a pull request, but I'm so new to this code I'm not really sure what form or intent is preferred by the authors.

@ricardograca

This comment has been minimized.

Show comment
Hide comment
@ricardograca

ricardograca Jan 28, 2013

Small problem. Are you sure the access-control-request-headers header will always be separated with ", " and not ","?

ricardograca commented Jan 28, 2013

Small problem. Are you sure the access-control-request-headers header will always be separated with ", " and not ","?

@benjie

This comment has been minimized.

Show comment
Hide comment
@benjie

benjie Jan 28, 2013

@ricardograca I believe this is what the RFCs/specs state, but I haven't checked. You can .split(/, */) if you prefer. I've never seen them without the spaces.

benjie commented Jan 28, 2013

@ricardograca I believe this is what the RFCs/specs state, but I haven't checked. You can .split(/, */) if you prefer. I've never seen them without the spaces.

@benjie

This comment has been minimized.

Show comment
Hide comment
@benjie

benjie Jan 28, 2013

It might be worth adding origin and accept to the default ALLOW_HEADERS too?

benjie commented Jan 28, 2013

It might be worth adding origin and accept to the default ALLOW_HEADERS too?

@mcavage

This comment has been minimized.

Show comment
Hide comment
@mcavage

mcavage Jan 29, 2013

Contributor

Sorry for the delay, I was traveling.

@benjie - this loop is out of scope of what is passed into headers for the plugin - that's what I was saying above - the default for preflight requests is like what restify 1.4 was - you will need to override that with a custom server.opts(...) definition(s). And yes, good catch on the access-control-request-headers being a string, I'll fix that up - this also needs a bunch of regression tests, but I was in a hurry, and wanted to get people the ability to use it. Anyway - origin you definitely don't want, as the browser is going to send that anyway, and I believe accept is in the same boat, but I'm not 100% sure w/o validating in a bunch of browsers - I can just put it in :)

@ricadograca/@benjie - they can be separated either way, I use this foo.split(/\s*,\s*/) in most places to parse header values.

Contributor

mcavage commented Jan 29, 2013

Sorry for the delay, I was traveling.

@benjie - this loop is out of scope of what is passed into headers for the plugin - that's what I was saying above - the default for preflight requests is like what restify 1.4 was - you will need to override that with a custom server.opts(...) definition(s). And yes, good catch on the access-control-request-headers being a string, I'll fix that up - this also needs a bunch of regression tests, but I was in a hurry, and wanted to get people the ability to use it. Anyway - origin you definitely don't want, as the browser is going to send that anyway, and I believe accept is in the same boat, but I'm not 100% sure w/o validating in a bunch of browsers - I can just put it in :)

@ricadograca/@benjie - they can be separated either way, I use this foo.split(/\s*,\s*/) in most places to parse header values.

@benjie

This comment has been minimized.

Show comment
Hide comment
@benjie

benjie Jan 29, 2013

you will need to override that with a custom server.opts(...) definition(s)

Okay, I'll take a look at doing that :)

Anyway - origin you definitely don't want, as the browser is going to send that anyway, and I believe accept is in the same boat, but I'm not 100% sure w/o validating in a bunch of browsers - I can just put it in :)

My apologies, you're right of course - I got confused and added the unnecessary headers to my access-control-request-headers in requests 😫

benjie commented Jan 29, 2013

you will need to override that with a custom server.opts(...) definition(s)

Okay, I'll take a look at doing that :)

Anyway - origin you definitely don't want, as the browser is going to send that anyway, and I believe accept is in the same boat, but I'm not 100% sure w/o validating in a bunch of browsers - I can just put it in :)

My apologies, you're right of course - I got confused and added the unnecessary headers to my access-control-request-headers in requests 😫

mcavage pushed a commit that referenced this issue Jan 30, 2013

mcavage pushed a commit that referenced this issue Jan 30, 2013

@mcavage

This comment has been minimized.

Show comment
Hide comment
@mcavage

mcavage Jan 30, 2013

Contributor

Closing this out, thanks for the catches!

Contributor

mcavage commented Jan 30, 2013

Closing this out, thanks for the catches!

@mcavage mcavage closed this Jan 30, 2013

@remotevision

This comment has been minimized.

Show comment
Hide comment
@remotevision

remotevision Feb 5, 2013

I'm sorry, but I'm still not able to get this working. Here is what I have.

restify@2.1.1

// server.js
...
server.use(restify.acceptParser(server.acceptable));
server.use(restify.queryParser());
server.use(restify.bodyParser());
server.use(restify.authorizationParser());
//server.use(authenticate);
server.use(restify.CORS());
server.use(restify.fullResponse());
...

// curl request
$ curl -isS http://127.0.0.1:3000/venues -H 'origin: foo.com' -X OPTIONS -H 'access-control-request-method: POST'

// response
HTTP/1.1 405 Method Not Allowed
Allow: GET, POST
Content-Type: application/json
Content-Length: 67
Date: Tue, 05 Feb 2013 21:36:33 GMT
Connection: keep-alive

Any idea what I could be doing wrong?

remotevision commented Feb 5, 2013

I'm sorry, but I'm still not able to get this working. Here is what I have.

restify@2.1.1

// server.js
...
server.use(restify.acceptParser(server.acceptable));
server.use(restify.queryParser());
server.use(restify.bodyParser());
server.use(restify.authorizationParser());
//server.use(authenticate);
server.use(restify.CORS());
server.use(restify.fullResponse());
...

// curl request
$ curl -isS http://127.0.0.1:3000/venues -H 'origin: foo.com' -X OPTIONS -H 'access-control-request-method: POST'

// response
HTTP/1.1 405 Method Not Allowed
Allow: GET, POST
Content-Type: application/json
Content-Length: 67
Date: Tue, 05 Feb 2013 21:36:33 GMT
Connection: keep-alive

Any idea what I could be doing wrong?

@remotevision

This comment has been minimized.

Show comment
Hide comment
@remotevision

remotevision Feb 5, 2013

One more thing...

I'm trying to call my restify api using a backbone.js web app hosted on a remote server. It appears backbone attempts a preflight request, which then gets denied by my restify api with 405 error.

I added the following to my venues.js route:
// create method in venues.js
....
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
....

// request headers from backbone.js app
OPTIONS /venues HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: http://localhost:3001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17
Access-Control-Request-Headers: accept, origin, content-type
Accept: /
Referer: http://localhost:3001/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

remotevision commented Feb 5, 2013

One more thing...

I'm trying to call my restify api using a backbone.js web app hosted on a remote server. It appears backbone attempts a preflight request, which then gets denied by my restify api with 405 error.

I added the following to my venues.js route:
// create method in venues.js
....
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
....

// request headers from backbone.js app
OPTIONS /venues HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: http://localhost:3001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17
Access-Control-Request-Headers: accept, origin, content-type
Accept: /
Referer: http://localhost:3001/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

@jfieber

This comment has been minimized.

Show comment
Hide comment
@jfieber

jfieber Feb 6, 2013

Feedback...

The CORS headers should be removed from the full_response plugin.

The preflight and simple request handling need to be better coordinated. It doesn't do well to have support for credentialed requests in one path but not the other. Or a knob for expose-headers but not accept-headers. What I've done, hacking away at the cors plugin, is to have it return both a "regular" handler and a preflight handler, used something like this:

  var cors = require('path/to/my-cors-plugin')({
    credentials: true,
    allow_headers: ['authorization'],
    expose_headers: ['x-my-custom-header'],
  });
  restify_server.use(cors.simple);
  restify_server.on('MethodNotAllowed', cors.preflight);

I don't know if that is a direction you are interested in taking things, but it does provide a little better coordination between preflight and simple requests.

jfieber commented Feb 6, 2013

Feedback...

The CORS headers should be removed from the full_response plugin.

The preflight and simple request handling need to be better coordinated. It doesn't do well to have support for credentialed requests in one path but not the other. Or a knob for expose-headers but not accept-headers. What I've done, hacking away at the cors plugin, is to have it return both a "regular" handler and a preflight handler, used something like this:

  var cors = require('path/to/my-cors-plugin')({
    credentials: true,
    allow_headers: ['authorization'],
    expose_headers: ['x-my-custom-header'],
  });
  restify_server.use(cors.simple);
  restify_server.on('MethodNotAllowed', cors.preflight);

I don't know if that is a direction you are interested in taking things, but it does provide a little better coordination between preflight and simple requests.

@mcavage

This comment has been minimized.

Show comment
Hide comment
@mcavage

mcavage Feb 18, 2013

Contributor

Hi John,

Yes, that actually does seem more sane, which is to bundle the preflight bits into the CORS plugin, instead of like it is now. I'll chip away at that as I get some time, unless somebody else (you? ;) ) wants to kick a PR over.

Contributor

mcavage commented Feb 18, 2013

Hi John,

Yes, that actually does seem more sane, which is to bundle the preflight bits into the CORS plugin, instead of like it is now. I'll chip away at that as I get some time, unless somebody else (you? ;) ) wants to kick a PR over.

@l8nite

This comment has been minimized.

Show comment
Hide comment
@l8nite

l8nite Mar 21, 2013

Any news? I had to hack lib/router.js to get the access-control-allow-headers to include some custom ones I need sent. =/

l8nite commented Mar 21, 2013

Any news? I had to hack lib/router.js to get the access-control-allow-headers to include some custom ones I need sent. =/

@ricardograca

This comment has been minimized.

Show comment
Hide comment
@ricardograca

ricardograca Mar 21, 2013

@l8nite You were better off just creating your own custom unknownMethodHandler.

ricardograca commented Mar 21, 2013

@l8nite You were better off just creating your own custom unknownMethodHandler.

@l8nite

This comment has been minimized.

Show comment
Hide comment
@l8nite

l8nite Mar 21, 2013

That's what I ended up doing. Thanks for the example!

  • Shaun

l8nite commented Mar 21, 2013

That's what I ended up doing. Thanks for the example!

  • Shaun
@pghmatt

This comment has been minimized.

Show comment
Hide comment
@pghmatt

pghmatt Mar 23, 2013

@ricardograca Thanks for sharing your solution! I'm calling services from dojo on another domain which is why I needed a solution. Your solution was receiving the options request and responding but my service was never called and in firebug I saw the request didn't finish loading. After some digging, I modified one line in your solution and it's working great! Here's the modified version in case it helps anyone else:

function unknownMethodHandler(req, res) {
  if (req.method.toLowerCase() === 'options') {
      console.log('received an options method request');
    var allowHeaders = ['Accept', 'Accept-Version', 'Content-Type', 'Api-Version', 'Origin', 'X-Requested-With']; // added Origin & X-Requested-With

    if (res.methods.indexOf('OPTIONS') === -1) res.methods.push('OPTIONS');

    res.header('Access-Control-Allow-Credentials', true);
    res.header('Access-Control-Allow-Headers', allowHeaders.join(', '));
    res.header('Access-Control-Allow-Methods', res.methods.join(', '));
    res.header('Access-Control-Allow-Origin', req.headers.origin);

    return res.send(204);
  }
  else
    return res.send(new restify.MethodNotAllowedError());
}

server.on('MethodNotAllowed', unknownMethodHandler);

pghmatt commented Mar 23, 2013

@ricardograca Thanks for sharing your solution! I'm calling services from dojo on another domain which is why I needed a solution. Your solution was receiving the options request and responding but my service was never called and in firebug I saw the request didn't finish loading. After some digging, I modified one line in your solution and it's working great! Here's the modified version in case it helps anyone else:

function unknownMethodHandler(req, res) {
  if (req.method.toLowerCase() === 'options') {
      console.log('received an options method request');
    var allowHeaders = ['Accept', 'Accept-Version', 'Content-Type', 'Api-Version', 'Origin', 'X-Requested-With']; // added Origin & X-Requested-With

    if (res.methods.indexOf('OPTIONS') === -1) res.methods.push('OPTIONS');

    res.header('Access-Control-Allow-Credentials', true);
    res.header('Access-Control-Allow-Headers', allowHeaders.join(', '));
    res.header('Access-Control-Allow-Methods', res.methods.join(', '));
    res.header('Access-Control-Allow-Origin', req.headers.origin);

    return res.send(204);
  }
  else
    return res.send(new restify.MethodNotAllowedError());
}

server.on('MethodNotAllowed', unknownMethodHandler);
@ivankravchenko

This comment has been minimized.

Show comment
Hide comment
@ivankravchenko

ivankravchenko May 14, 2013

We made correct handling for CORS and preflight requests with support for Cookies for authorization.

# CORS headers
server.use (req, res, next) ->
    res.header 'Access-Control-Allow-Origin', req.headers.origin if req.headers.origin
    res.header 'Access-Control-Allow-Credentials', 'true'
    res.header 'Access-Control-Allow-Headers', 'X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control-Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id'
    res.header 'Access-Control-Expose-Headers', 'Set-Cookie'
    next()

# Preflight requests
server.opts '.*', (req, res, next) ->
    if req.headers.origin and req.headers['access-control-request-method']
        res.header 'Access-Control-Allow-Origin', req.headers.origin
        res.header 'Access-Control-Allow-Credentials', 'true'
        res.header 'Access-Control-Allow-Headers', 'X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control-Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id'
        res.header 'Access-Control-Expose-Headers', 'Set-Cookie'
        res.header 'Allow', req.headers['access-control-request-method']
        res.header 'Access-Control-Allow-Methods', req.headers['access-control-request-method']
        req.log.info { url:req.url, method:req.headers['access-control-request-method'] }, "Preflight"
        res.send 204
        next()
    else
        res.send 404
        next()

ivankravchenko commented May 14, 2013

We made correct handling for CORS and preflight requests with support for Cookies for authorization.

# CORS headers
server.use (req, res, next) ->
    res.header 'Access-Control-Allow-Origin', req.headers.origin if req.headers.origin
    res.header 'Access-Control-Allow-Credentials', 'true'
    res.header 'Access-Control-Allow-Headers', 'X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control-Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id'
    res.header 'Access-Control-Expose-Headers', 'Set-Cookie'
    next()

# Preflight requests
server.opts '.*', (req, res, next) ->
    if req.headers.origin and req.headers['access-control-request-method']
        res.header 'Access-Control-Allow-Origin', req.headers.origin
        res.header 'Access-Control-Allow-Credentials', 'true'
        res.header 'Access-Control-Allow-Headers', 'X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control-Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id'
        res.header 'Access-Control-Expose-Headers', 'Set-Cookie'
        res.header 'Allow', req.headers['access-control-request-method']
        res.header 'Access-Control-Allow-Methods', req.headers['access-control-request-method']
        req.log.info { url:req.url, method:req.headers['access-control-request-method'] }, "Preflight"
        res.send 204
        next()
    else
        res.send 404
        next()
@ivankravchenko

This comment has been minimized.

Show comment
Hide comment
@nmathon

This comment has been minimized.

Show comment
Hide comment
@nmathon

nmathon Jul 31, 2013

Just for information, because it takes me a 2 hours to find this solution, if you use OAuth2, you must add 'Authorization' to headers of the Preflight response :

function unknownMethodHandler(req, res) {
  if (req.method.toLowerCase() === 'options') {
      console.log('received an options method request');
    var allowHeaders = ['Accept', 'Accept-Version', 'Content-Type', 'Api-Version', 'Origin', 'X-Requested-With', 'Authorization']; // added Origin & X-Requested-With & **Authorization**

    if (res.methods.indexOf('OPTIONS') === -1) res.methods.push('OPTIONS');

    res.header('Access-Control-Allow-Credentials', true);
    res.header('Access-Control-Allow-Headers', allowHeaders.join(', '));
    res.header('Access-Control-Allow-Methods', res.methods.join(', '));
    res.header('Access-Control-Allow-Origin', req.headers.origin);

    return res.send(200);
  }
  else
    return res.send(new restify.MethodNotAllowedError());
}

server.on('MethodNotAllowed', unknownMethodHandler);

nmathon commented Jul 31, 2013

Just for information, because it takes me a 2 hours to find this solution, if you use OAuth2, you must add 'Authorization' to headers of the Preflight response :

function unknownMethodHandler(req, res) {
  if (req.method.toLowerCase() === 'options') {
      console.log('received an options method request');
    var allowHeaders = ['Accept', 'Accept-Version', 'Content-Type', 'Api-Version', 'Origin', 'X-Requested-With', 'Authorization']; // added Origin & X-Requested-With & **Authorization**

    if (res.methods.indexOf('OPTIONS') === -1) res.methods.push('OPTIONS');

    res.header('Access-Control-Allow-Credentials', true);
    res.header('Access-Control-Allow-Headers', allowHeaders.join(', '));
    res.header('Access-Control-Allow-Methods', res.methods.join(', '));
    res.header('Access-Control-Allow-Origin', req.headers.origin);

    return res.send(200);
  }
  else
    return res.send(new restify.MethodNotAllowedError());
}

server.on('MethodNotAllowed', unknownMethodHandler);
@ivankravchenko

This comment has been minimized.

Show comment
Hide comment
@ivankravchenko

ivankravchenko Aug 1, 2013

updated se7ensky-restify-preflight, thanks

ivankravchenko commented Aug 1, 2013

updated se7ensky-restify-preflight, thanks

@pcimino

This comment has been minimized.

Show comment
Hide comment
@pcimino

pcimino Aug 17, 2013

I'm having an issue with POST requests with ,xhrFields: {withCredentials: true} and using se7ensky-restify-preflight. The preflight appears to be working on the OPTIONS, I can see

Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control-Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id
Access-Control-Allow-Methods:POST
Access-Control-Allow-Origin:http://localhost:8888

But then the POST request fails because:
Access-Control-Allow-Origin:

So if I use credentials the cookie gets created and the POST fails, if I turn off the credentials, the cookie doesn't get created, and the POST succeeds, and I can see the header that the allow origin is '
'.

Am I mis-configuring (or not configuring) something? What am I missing here?

I've updated to the latest versions of restify and preflight, nodejs v0.10.13

pcimino commented Aug 17, 2013

I'm having an issue with POST requests with ,xhrFields: {withCredentials: true} and using se7ensky-restify-preflight. The preflight appears to be working on the OPTIONS, I can see

Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control-Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id
Access-Control-Allow-Methods:POST
Access-Control-Allow-Origin:http://localhost:8888

But then the POST request fails because:
Access-Control-Allow-Origin:

So if I use credentials the cookie gets created and the POST fails, if I turn off the credentials, the cookie doesn't get created, and the POST succeeds, and I can see the header that the allow origin is '
'.

Am I mis-configuring (or not configuring) something? What am I missing here?

I've updated to the latest versions of restify and preflight, nodejs v0.10.13

@ivankravchenko

This comment has been minimized.

Show comment
Hide comment
@ivankravchenko

ivankravchenko Aug 19, 2013

Please see example project: https://github.com/krava/se7ensky-restify-seed
Example queries:

$ curl -X OPTIONS -H 'Origin: http://domain.tld:5000/' -H 'Access-Control-Request-Method: POST' -v http://localhost:3000/secure/check
< HTTP/1.1 204 No Content
< Access-Control-Allow-Origin: http://domain.tld:5000/
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control-Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id, Authorization
< Access-Control-Expose-Headers: Set-Cookie
< Allow: POST
< Access-Control-Allow-Methods: POST
< Date: Mon, 19 Aug 2013 17:40:33 GMT
< Connection: keep-alive
$ curl -X POST -H 'Origin: http://domain.tld:5000/' -v http://localhost:3000/secure/check
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: http://domain.tld:5000/
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control-Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id, Authorization
< Access-Control-Expose-Headers: Set-Cookie
< Content-Type: application/json
< Content-Length: 10
< Date: Mon, 19 Aug 2013 17:42:15 GMT
< Connection: keep-alive

Access-Control-Allow-Origin is always set to what I pass in Origin header.

I cannot figure out how you received error about "Access-Control-Allow-Origin: *". Please give me right curl request, so I can debug that. Or give me code example that don't work.

ivankravchenko commented Aug 19, 2013

Please see example project: https://github.com/krava/se7ensky-restify-seed
Example queries:

$ curl -X OPTIONS -H 'Origin: http://domain.tld:5000/' -H 'Access-Control-Request-Method: POST' -v http://localhost:3000/secure/check
< HTTP/1.1 204 No Content
< Access-Control-Allow-Origin: http://domain.tld:5000/
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control-Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id, Authorization
< Access-Control-Expose-Headers: Set-Cookie
< Allow: POST
< Access-Control-Allow-Methods: POST
< Date: Mon, 19 Aug 2013 17:40:33 GMT
< Connection: keep-alive
$ curl -X POST -H 'Origin: http://domain.tld:5000/' -v http://localhost:3000/secure/check
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: http://domain.tld:5000/
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control-Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id, Authorization
< Access-Control-Expose-Headers: Set-Cookie
< Content-Type: application/json
< Content-Length: 10
< Date: Mon, 19 Aug 2013 17:42:15 GMT
< Connection: keep-alive

Access-Control-Allow-Origin is always set to what I pass in Origin header.

I cannot figure out how you received error about "Access-Control-Allow-Origin: *". Please give me right curl request, so I can debug that. Or give me code example that don't work.

@pcimino

This comment has been minimized.

Show comment
Hide comment
@pcimino

pcimino Aug 20, 2013

Short answer it's solved. (Thanks much for your help and library). The long answer is, either because I'm on a Windows cURL or I'm just not familiar with it, I couldn't build a good example. So I duplicated my code base to strip out the DB and REST code and create a simple example. That's when I found a duplicate set of calls, that apparently interfered with your preflight lib. These lines weren't in my server file, but in a configuration file it pulled in, so I didn't notice them before.

app.use(restify.CORS());
app.use(restify.fullResponse());

In case you're curious, this is the REST demo I created as a back end https://github.com/pcimino/nodejs-restify-mongodb. Currently working on an Enyo front end (not releasable yet)

pcimino commented Aug 20, 2013

Short answer it's solved. (Thanks much for your help and library). The long answer is, either because I'm on a Windows cURL or I'm just not familiar with it, I couldn't build a good example. So I duplicated my code base to strip out the DB and REST code and create a simple example. That's when I found a duplicate set of calls, that apparently interfered with your preflight lib. These lines weren't in my server file, but in a configuration file it pulled in, so I didn't notice them before.

app.use(restify.CORS());
app.use(restify.fullResponse());

In case you're curious, this is the REST demo I created as a back end https://github.com/pcimino/nodejs-restify-mongodb. Currently working on an Enyo front end (not releasable yet)

@lymanlai

This comment has been minimized.

Show comment
Hide comment
@lymanlai

lymanlai Sep 10, 2013

just find out the solution:

restify.CORS.ALLOW_HEADERS.push('accept');
restify.CORS.ALLOW_HEADERS.push('sid');
restify.CORS.ALLOW_HEADERS.push('lang');
restify.CORS.ALLOW_HEADERS.push('origin');
restify.CORS.ALLOW_HEADERS.push('withcredentials');
restify.CORS.ALLOW_HEADERS.push('x-requested-with');
server.use(restify.CORS());

It works on my projects Http://yaha.me

lymanlai commented Sep 10, 2013

just find out the solution:

restify.CORS.ALLOW_HEADERS.push('accept');
restify.CORS.ALLOW_HEADERS.push('sid');
restify.CORS.ALLOW_HEADERS.push('lang');
restify.CORS.ALLOW_HEADERS.push('origin');
restify.CORS.ALLOW_HEADERS.push('withcredentials');
restify.CORS.ALLOW_HEADERS.push('x-requested-with');
server.use(restify.CORS());

It works on my projects Http://yaha.me

@lobodpav

This comment has been minimized.

Show comment
Hide comment
@lobodpav

lobodpav Jan 27, 2014

Found this issue after a few hours of googling and the CORS topic would definitely deserve its space in the doc where it is missing completely now.

lobodpav commented Jan 27, 2014

Found this issue after a few hours of googling and the CORS topic would definitely deserve its space in the doc where it is missing completely now.

@diasdavid

This comment has been minimized.

Show comment
Hide comment
@diasdavid

diasdavid commented Feb 23, 2014

+1 to @lobodpav

@DTrejo

This comment has been minimized.

Show comment
Hide comment
@DTrejo

DTrejo commented Apr 4, 2014

@rprieto

This comment has been minimized.

Show comment
Hide comment
@rprieto

rprieto May 7, 2014

Contributor

@jfieber, is your customised CORS module in a shareable state?

I think this is what makes the most sense too. After configuring restify.CORS(opts), it would be great to use it for both preflight & simple requests (including list of exposed headers, allowed origins, etc).

As a small note: the MethodNotAllowed handlers above seem to always accept preflights. The spec says it's acceptable, however for security reasons it would be nice to also do origin-matching if the CORS module is configured with a list of origins.

Update: my attempt at a new spec-tested CORS middleware over here.

Contributor

rprieto commented May 7, 2014

@jfieber, is your customised CORS module in a shareable state?

I think this is what makes the most sense too. After configuring restify.CORS(opts), it would be great to use it for both preflight & simple requests (including list of exposed headers, allowed origins, etc).

As a small note: the MethodNotAllowed handlers above seem to always accept preflights. The spec says it's acceptable, however for security reasons it would be nice to also do origin-matching if the CORS module is configured with a list of origins.

Update: my attempt at a new spec-tested CORS middleware over here.

@vchatterji

This comment has been minimized.

Show comment
Hide comment
@vchatterji

vchatterji Jun 27, 2016

If your pre-flight OPTIONS request specifies which headers it wants to send:
'access-control-request-headers': 'authorization, content-type'

THEN, these headers should be allowed in a subsequent CORS request:

restify.CORS.ALLOW_HEADERS.push("authorization");
restify.CORS.ALLOW_HEADERS.push("content-type");

If the requested headers are not allowed, then the OPTIONS request will itself fail:

HTTP/1.1 405 Method Not Allowed
Allow: POST
Content-Type: application/json
Content-Length: 67
Date: Mon, 27 Jun 2016 15:30:37 GMT
Connection: keep-alive

The response indicates that if the client was to send a request with the headers authorization and content-type, the server would NOT allow that method (as these headers are not enabled).

vchatterji commented Jun 27, 2016

If your pre-flight OPTIONS request specifies which headers it wants to send:
'access-control-request-headers': 'authorization, content-type'

THEN, these headers should be allowed in a subsequent CORS request:

restify.CORS.ALLOW_HEADERS.push("authorization");
restify.CORS.ALLOW_HEADERS.push("content-type");

If the requested headers are not allowed, then the OPTIONS request will itself fail:

HTTP/1.1 405 Method Not Allowed
Allow: POST
Content-Type: application/json
Content-Length: 67
Date: Mon, 27 Jun 2016 15:30:37 GMT
Connection: keep-alive

The response indicates that if the client was to send a request with the headers authorization and content-type, the server would NOT allow that method (as these headers are not enabled).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment