node.jsexpressrestmongooseember-data

Express REST server losing payload from Ember-data PUT


I am extending the standard Ember example project (in coffeescript) to talk to an express/mongoose RESTful server. I have successfully fetched all and single records via POSTS using the recommended:

Find        GET     /people/123
Find All    GET     /people

Now when attempting to update a record via ember-data PUT, triggered through the Ember adapter.

Update      PUT     /people/123

And it's not working.

Profiling: client

I am profiling the client side with Chrome dev tools, and server side with console.log. Here is what i'm seeing on the client side. Ember-data makes a PUT and an OPTIONS call to the server.

enter image description here

In the PUT I'm seeing that the payload contains the user edits, and the format looks correct.

enter image description here

The response tab shows a bunch of nonsense, so I'm assuming the problem is on the server side.

Profiling: server

On the server side, when I dump the request variable I get this. The body is received as {post: {}}, i.e. correct structure but empty of content. And the correct mongo id is received.

enter image description here

Here is a stacktrace from the server router:

Trace
    at /home/vagrant/restl/node_modules/restgen/lib/routes.js:68:15
    at callbacks (/home/vagrant/restl/node_modules/express/lib/router/index.js:161:37)
    at param (/home/vagrant/restl/node_modules/express/lib/router/index.js:135:11)
    at param (/home/vagrant/restl/node_modules/express/lib/router/index.js:132:11)
    at param (/home/vagrant/restl/node_modules/express/lib/router/index.js:132:11)
    at pass (/home/vagrant/restl/node_modules/express/lib/router/index.js:142:5)
    at Router._dispatch (/home/vagrant/restl/node_modules/express/lib/router/index.js:170:5)
    at Object.router (/home/vagrant/restl/node_modules/express/lib/router/index.js:33:10)
    at next (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/proto.js:190:15)
    at Object.session [as handle] (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/middleware/session.js:301:7)
    at next (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/proto.js:190:15)
    at Object.cookieParser [as handle] (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/middleware/cookieParser.js:60:5)
    at next (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/proto.js:190:15)
    at Object.allowCrossDomain [as handle] (/home/vagrant/restl/app.js:21:5)
    at next (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/proto.js:190:15)
    at Object.methodOverride [as handle] (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/middleware/methodOverride.js:49:5)
    at next (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/proto.js:190:15)
    at multipart (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/middleware/multipart.js:60:27)
    at /home/vagrant/restl/node_modules/express/node_modules/connect/lib/middleware/bodyParser.js:57:9
    at urlencoded (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/middleware/urlencoded.js:48:27)
    at /home/vagrant/restl/node_modules/express/node_modules/connect/lib/middleware/bodyParser.js:55:7
    at IncomingMessage.<anonymous> (/home/vagrant/restl/node_modules/express/node_modules/connect/lib/middleware/json.js:82:9)
    at IncomingMessage.EventEmitter.emit (events.js:92:17)
    at _stream_readable.js:910:16
    at process._tickCallback (node.js:415:13)

What's wrong with my express REST server?

Server side code

Here are the relevant parts of the server code, which is forked from npm restgen.

var express = require('express')
  , http = require('http')
  , path = require('path')
  , restgen = require('restgen');

var app = express()
  , mongoose = require('mongoose');

mongoose.connect('mongodb://localhost/rest');

// development only
if ('development' == app.get('env')) {
  app.use(express.logger({ format: '\x1b[1m :date \x1b[1m:method\x1b[0m \x1b[33m:url\x1b[0m :response-time ms\x1b[0m :status' }));
}

//CORS middleware
var allowCrossDomain = function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type');
    next();
}
// all environments
app.configure(function() {
  app.set('root', __dirname);
  app.set('port', process.env.PORT || 3000); //3000
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.favicon());
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(allowCrossDomain);
  app.use(express.cookieParser('your secret here'));
  app.use(express.session());
  app.use(app.router);
  app.use(express.static(path.join(__dirname, 'public')));
  app.use(restgen.ErrorHandler)
});

restgen.Initialize(app, mongoose);

app.use(function(req, res, next){
  next(restgen.RestError.NotFound.insert(req.url));
});

// example of how to throw a 404
app.get('/404', function(req, res, next){
  next(restgen.RestError.NotFound.insert(req.url));
});

// example of how to throw a 500
app.get('/500', function(req, res, next){
  next(new Error('keyboard cat!'));
});

if(!module.parent) {
  http.createServer(app).listen(app.get('port'), function(){
    console.log('Express server listening on port ' + app.get('port'));
  });
}

exports.app = app;

Somewhere on the client side the request payload is getting lost. I'm happy to post additional parts of the server side code, such as routes, if it's relevant. Please ask in comments.

Client side code

Here is the Ember side, which I'm pretty sure is correct.

# ===== Adapter =====
# Extended to handle mongo's _id as primarykey
App.Adapter = DS.RESTAdapter.extend(
 serializer: DS.RESTSerializer.extend(
  primaryKey: (type) -> "_id"
  )   
)
DS.RESTAdapter.reopen({url: 'http://localhost:3000'});

# ===== Store =====
App.Store = DS.Store.extend(
  revision: 12
  adapter: App.Adapter
)

# ===== Controller =====
module.exports = App.PostController = Ember.ObjectController.extend
    save: ->
        @get("store").commit()
        @get("target.router").transitionTo "posts.index"

# ==== Model ====
module.exports = App.Post = DS.Model.extend
    title: DS.attr 'string'
    author: DS.attr 'string'
    intro: DS.attr 'string'
    extended: DS.attr 'string'
    publishedAt: DS.attr 'date'

Update #1

Version information from npm ls

vagrant@precise32:~/brunch-ember$ npm ls
brunch-with-ember-reloaded@0.0.3 /home/vagrant/brunch-ember
├─┬ auto-reload-brunch@1.5.2
│ └─┬ ws@0.4.20
│   ├── commander@0.6.1
│   ├── options@0.0.5
│   └── tinycolor@0.0.1
├─┬ chai@1.7.2
│ └── assertion-error@1.0.0
├─┬ clean-css-brunch@1.5.1
│ └─┬ clean-css@0.10.2
│   └─┬ commander@1.1.1
│     └── keypress@0.1.0
├─┬ coffee-script-brunch@1.5.1
│ └── coffee-script@1.6.3
├── css-brunch@1.5.1
├─┬ dc@1.4.0 extraneous
│ ├── crossfilter@1.2.0
│ └─┬ d3@3.2.6
│   └─┬ jsdom@0.5.7
│     ├─┬ contextify@0.1.6
│     │ └── bindings@1.1.1
│     ├── cssom@0.2.5
│     ├── cssstyle@0.2.3
│     ├── htmlparser@1.7.6
│     ├── nwmatcher@1.3.1
│     └─┬ request@2.22.0
│       ├── aws-sign@0.3.0
│       ├── cookie-jar@0.3.0
│       ├── forever-agent@0.5.0
│       ├─┬ form-data@0.0.8
│       │ ├── async@0.2.9
│       │ └─┬ combined-stream@0.0.4
│       │   └── delayed-stream@0.0.5
│       ├─┬ hawk@0.13.1
│       │ ├─┬ boom@0.4.2
│       │ │ └── hoek@0.9.1
│       │ ├── cryptiles@0.2.2
│       │ ├── hoek@0.8.5
│       │ └─┬ sntp@0.2.4
│       │   └── hoek@0.9.1
│       ├─┬ http-signature@0.10.0
│       │ ├── asn1@0.1.11
│       │ ├── assert-plus@0.1.2
│       │ └── ctype@0.5.2
│       ├── json-stringify-safe@4.0.0
│       ├── mime@1.2.9
│       ├── node-uuid@1.4.0
│       ├── oauth-sign@0.3.0
│       ├── qs@0.6.5
│       └── tunnel-agent@0.3.0
├─┬ ember-handlebars-brunch@1.0.4 (git+ssh://git@github.com:bartsqueezy/ember-handlebars-brunch.git#19b9cfd141
│ └── coffee-script@1.6.2
├─┬ express@3.3.4
│ ├── buffer-crc32@0.2.1
│ ├─┬ commander@1.2.0
│ │ └── keypress@0.1.0
│ ├─┬ connect@2.8.4
│ │ ├── bytes@0.2.0
│ │ ├── formidable@1.0.14
│ │ ├── pause@0.0.1
│ │ ├── qs@0.6.5
│ │ └── uid2@0.0.2
│ ├── cookie@0.1.0
│ ├── cookie-signature@1.0.1
│ ├── debug@0.7.2
│ ├── fresh@0.1.0
│ ├── methods@0.0.1
│ ├── mkdirp@0.3.5
│ ├── range-parser@0.0.4
│ └─┬ send@0.1.3
│   └── mime@1.2.9
├─┬ jade@0.32.0
│ ├── character-parser@1.0.2
│ ├─┬ commander@1.2.0
│ │ └── keypress@0.1.0
│ ├─┬ constantinople@1.0.1
│ │ └─┬ uglify-js@2.3.6
│ │   ├── async@0.2.9
│ │   ├─┬ optimist@0.3.7
│ │   │ └── wordwrap@0.0.2
│ │   └─┬ source-map@0.1.25
│ │     └── amdefine@0.0.5
│ ├── mkdirp@0.3.5
│ ├─┬ monocle@0.1.48
│ │ └─┬ readdirp@0.2.5
│ │   └─┬ minimatch@0.2.12
│ │     ├── lru-cache@2.3.0
│ │     └── sigmund@1.0.0
│ ├─┬ transformers@2.0.1
│ │ ├─┬ css@1.0.8
│ │ │ ├── css-parse@1.0.4
│ │ │ └── css-stringify@1.0.5
│ │ ├─┬ promise@2.0.0
│ │ │ └── is-promise@1.0.0
│ │ └─┬ uglify-js@2.2.5
│ │   ├─┬ optimist@0.3.7
│ │   │ └── wordwrap@0.0.2
│ │   └─┬ source-map@0.1.25
│ │     └── amdefine@0.0.5
│ └─┬ with@1.1.0
│   └─┬ uglify-js@2.3.6
│     ├── async@0.2.9
│     ├─┬ optimist@0.3.7
│     │ └── wordwrap@0.0.2
│     └─┬ source-map@0.1.25
│       └── amdefine@0.0.5
├── javascript-brunch@1.5.1
├─┬ karma@0.8.1
│ ├── chokidar@0.5.3
│ ├── coffee-script@1.4.0
│ ├── colors@0.6.0-1
│ ├── dateformat@1.0.2-1.2.3
│ ├─┬ glob@3.1.20
│ │ ├── graceful-fs@1.2.2
│ │ └── inherits@1.0.0
│ ├── growly@1.1.1
│ ├─┬ http-proxy@0.10.0
│ │ ├── pkginfo@0.2.3
│ │ └─┬ utile@0.1.7
│ │   ├── async@0.1.22
│ │   ├── deep-equal@0.0.0
│ │   ├── i@0.3.1
│ │   ├── mkdirp@0.3.5
│ │   ├── ncp@0.2.7
│ │   └── rimraf@1.0.9
│ ├─┬ istanbul@0.1.22
│ │ ├── abbrev@1.0.4
│ │ ├── async@0.1.22
│ │ ├─┬ escodegen@0.0.23
│ │ │ ├── esprima@1.0.3
│ │ │ ├── estraverse@0.0.4
│ │ │ └─┬ source-map@0.1.25
│ │ │   └── amdefine@0.0.5
│ │ ├── esprima@0.9.9
│ │ ├── fileset@0.1.5
│ │ ├─┬ handlebars@1.0.12
│ │ │ └─┬ uglify-js@2.3.6
│ │ │   ├── async@0.2.9
│ │ │   └─┬ source-map@0.1.25
│ │ │     └── amdefine@0.0.5
│ │ ├── mkdirp@0.3.5
│ │ ├── nopt@2.0.0
│ │ ├── which@1.0.5
│ │ └── wordwrap@0.0.2
│ ├─┬ LiveScript@1.0.1
│ │ └── prelude-ls@1.0.0
│ ├── lodash@0.9.2 invalid
│ ├─┬ log4js@0.5.6
│ │ └── async@0.1.15
│ ├── mime@1.2.7
│ ├─┬ minimatch@0.2.9
│ │ ├── lru-cache@2.0.4
│ │ └── sigmund@1.0.0
│ ├─┬ optimist@0.3.5
│ │ └── wordwrap@0.0.2
│ ├── pause@0.0.1
│ ├── q@0.8.12
│ ├─┬ rimraf@2.1.4
│ │ └── graceful-fs@1.2.2
│ ├─┬ socket.io@0.9.13
│ │ ├── base64id@0.1.0
│ │ ├── policyfile@0.0.4
│ │ ├── redis@0.7.3
│ │ └─┬ socket.io-client@0.9.11
│ │   ├─┬ active-x-obfuscator@0.0.1
│ │   │ └── zeparser@0.0.5
│ │   ├── uglify-js@1.2.5
│ │   ├─┬ ws@0.4.27
│ │   │ ├── commander@0.6.1
│ │   │ ├── options@0.0.5
│ │   │ └── tinycolor@0.0.1
│ │   └── xmlhttprequest@1.4.2
│ └── xmlbuilder@0.4.2
├─┬ mocha@1.12.0
│ ├── commander@0.6.1
│ ├── debug@0.7.2
│ ├── diff@1.0.2
│ ├─┬ glob@3.2.1
│ │ ├── graceful-fs@1.2.2
│ │ ├── inherits@1.0.0
│ │ └─┬ minimatch@0.2.12
│ │   ├── lru-cache@2.3.0
│ │   └── sigmund@1.0.0
│ ├── growl@1.7.0
│ ├─┬ jade@0.26.3
│ │ └── mkdirp@0.3.0
│ ├── mkdirp@0.3.5
│ └── ms@0.3.0
├── moment@2.0.0
├─┬ mongoose@3.6.14
│ ├── hooks@0.2.1
│ ├─┬ mongodb@1.3.11
│ │ ├── bson@0.1.9
│ │ └── kerberos@0.0.3
│ ├── mpath@0.1.1
│ ├─┬ mpromise@0.2.1
│ │ └── sliced@0.0.4
│ ├── ms@0.1.0
│ ├── muri@0.3.1
│ ├── regexp-clone@0.0.1
│ └── sliced@0.0.3
├─┬ restgen@1.0.2
│ ├── UNMET DEPENDENCY cli-color 0.2.2
│ ├── UNMET DEPENDENCY commander https://github.com/alexferreira/commander.js/tarball/master
│ ├── UNMET DEPENDENCY ejs latest
│ ├── UNMET DEPENDENCY fleck 0.5.1
│ ├── UNMET DEPENDENCY fs-extra 0.6.1
│ └── UNMET DEPENDENCY rsvp-that-works 1.2.0
├── showdown@0.3.1
├─┬ stylus-brunch@1.5.1
│ ├─┬ nib@0.9.2
│ │ └─┬ stylus@0.31.0
│ │   ├── cssom@0.2.5
│ │   ├── debug@0.7.2
│ │   └── mkdirp@0.3.5
│ ├─┬ node-sprite@0.1.1
│ │ ├── coffee-script@1.3.3
│ │ ├── imagemagick@0.1.2
│ │ ├─┬ seq@0.3.5
│ │ │ ├─┬ chainsaw@0.0.9
│ │ │ │ └── traverse@0.3.9
│ │ │ └─┬ hashish@0.0.4
│ │ │   └── traverse@0.6.3
│ │ ├── underscore@1.3.1
│ │ └── watch@0.5.1
│ └─┬ stylus@0.32.1
│   ├── cssom@0.2.5
│   ├── debug@0.7.2
│   └── mkdirp@0.3.5
├─┬ supertest@0.6.0
│ ├── methods@0.0.1
│ └─┬ superagent@0.10.0
│   ├─┬ better-assert@0.1.0
│   │ └── callsite@1.0.0
│   ├── cookiejar@1.3.0
│   ├── emitter-component@0.0.6
│   ├── formidable@1.0.9
│   ├── mime@1.2.5
│   └── qs@0.5.2
├── twitter-bootstrap@2.1.1
└─┬ uglify-js-brunch@1.5.1
  └─┬ uglify-js@2.2.5
    ├─┬ optimist@0.3.7
    │ └── wordwrap@0.0.2
    └─┬ source-map@0.1.25
      └── amdefine@0.0.5

Solution

  • I haven't dug in too far, but I think this might be a subtle difference between ember-data and restgen: restgen expects form data (title=foo&body=bar) unless the url ends in ".json". But ember data is uploading json without adding that to the URL.

    I haven't yet found any docs from either side indicating how to change the default behavior, but if that is indeed the issue then you should be able to figure out a workaround.

    Update: a quick way to check this is to right-click the ajax request in Chrome's Developer Tools, and choose "Copy as cURL", then paste that into a notepad and edit the url to include ".ajax" on the end, then run the command from the command line. If your server processes this correctly, then that's a good sign that we're on the right track.