Describe the bug
When Sapper middleware is active, nothing can be served anymore by fastify
Logs
N/A
To Reproduce
I took the sapper rollup template
Then I installed fastify and fastify-static
npm i fastify fastify-static
Then I changed server.js with the following
const fastify = require('fastify')({ logger: true });
const path = require('path');
// Register the static plugin and serve content from static folder
fastify.register(require('fastify-static'), {
root: path.join(__dirname, '../../..', 'static')
})
// Run the sapper middleware which will handle server-side rendering
fastify.use(require('@sapper/server').middleware());
fastify.listen(process.env.PORT, (err, address) => {
if (err) { throw err; }
fastify.log.info(`Server listening on ${address}`);
});
And ran npm run dev
Actual behavior
http://localhost:3000/favicon.png returns 404
Expected behavior
http://localhost:3000/favicon.png returns the favicon
If I remove the line fastify.use(require('@sapper/server').middleware());, the favicon is served
Information about your Sapper Installation:
Your operating system: Manjaro 18
Your hosting environment: Local
Sapper version 0.27.9
Svelte version 3.16.0
dynamic application.
Rollup
Severity
Workaround is to use sirv
It works well with sirv
fastify.use(require('sirv')('static'))
I dealt with this recently and I'm not sure if there's an easy way around it.
Unlike some other web frameworks, Fastify always runs all the middleware before figuring out which request handlers should run (see https://www.fastify.io/docs/latest/Lifecycle/). fastify-static doesn't act as a middleware, but instead installs route handlers to serve the static files, so Sapper will always run first regardless of the order in which you enable them. sirv and most other static-file-serving libraries work fine since they function as middleware, and so are able to handle the request before Sapper gets its chance.
I ended up doing the same thing, just using sirv for static file serving. If you do really want to use fastify-static, I can think of a couple options:
let sap = sapper.middleware();
fastify.route({
method: [ 'GET', 'POST', etc. ],
url: '/*',
handler: (request, reply) => {
return new Promise((resolve, reject) => {
let next = (err) => {
if(err) { reject(err) } else { resolve() };
};
sap(request, reply, next);
});
}
});
function sapperMiddleware() {
let sap = sapper.middleware();
return (req, res, next) => {
if (req.url.startsWith('/the_directory')) {
return next();
}
sap(req, res, next);
};
}
fastify.use(sapperMiddleware())
I used this approach in my own code since I wanted my Fastify routes to handle the /api prefix. Notably, the Sapper middleware does have an "ignore" option, but it assumes that req.path exists, which is not true on Fastify. Perhaps that would be considered a bug in the middleware, not sure.
One problem with my solution above is that fastify plugins such as fastify-compress will not apply to responses sent from the Sapper middleware. The code block below will install the middleware as a normal fastify route handler. This is the working version of approach 1 from my first post, but I should warn you that it's even more of a hack than my first attempt :)
let sap = sapper.middleware();
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'PATCH', 'POST', 'DELETE'],
url: '/*',
handler: async (request, reply) => {
let next = (err) => {
if (err) {
throw err;
} else {
reply.status(404).send();
}
};
request.url = request.raw.url;
request.originalUrl = request.raw.url;
reply.end = reply.send.bind(reply);
reply.setHeader = reply.header.bind(reply);
sap(request, reply, next);
return reply;
},
});
One problem with my solution above is that fastify plugins such as
fastify-compresswill not apply to responses sent from the Sapper middleware. The code block below will install the middleware as a normal fastify route handler. This is the working version of approach 1 from my first post, but I should warn you that it's even more of a hack than my first attempt :)let sap = sapper.middleware(); fastify.route({ method: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'PATCH', 'POST', 'DELETE'], url: '/*', handler: async (request, reply) => { let next = (err) => { if (err) { throw err; } else { reply.status(404).send(); } }; request.url = request.raw.url; request.originalUrl = request.raw.url; reply.end = reply.send.bind(reply); reply.setHeader = reply.header.bind(reply); sap(request, reply, next); return reply; }, });
It's not working on the sapper example...
It loads fine and I can go to the "about" tab, but the "blog" won't load and the following error is thrown on the server:
(node:8613) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'toLowerCase' of undefined
at handle_route (webpack:///./src/node_modules/@sapper/server.mjs?:28:29)
at Array.find_route (webpack:///./src/node_modules/@sapper/server.mjs?:90:5)
at nth_handler (webpack:///./src/node_modules/@sapper/server.mjs?:2609:14)
at eval (webpack:///./src/node_modules/@sapper/server.mjs?:2609:31)
at Array.eval (webpack:///./src/node_modules/@sapper/server.mjs?:2661:4)
at nth_handler (webpack:///./src/node_modules/@sapper/server.mjs?:2609:14)
at eval (webpack:///./src/node_modules/@sapper/server.mjs?:2609:31)
at Array.eval (webpack:///./src/node_modules/@sapper/server.mjs?:2661:4)
at nth_handler (webpack:///./src/node_modules/@sapper/server.mjs?:2609:14)
at eval (webpack:///./src/node_modules/@sapper/server.mjs?:2609:31)
(node:8613) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag--unhandled-rejections=strict(see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:8613) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Ok, if you can figure out what needs to be changed, let me know and I'll update the code snippet.
@Dindaleon Hi, have you figured this out?
EDIT:
Ok I figured it out.
You need to add request.method = request.raw.method;
There seems to be in general just a compatibility issue here. Fastify does not use express style req/res. Pretty much everything else does (and so does sapper) at least from my initial impression. I am also interested in getting fastify to work with it, but I don't think it's going to work playing whack-a-mole with basically setting the properties it expects on the reply object. It's just too subject to change - a lot of the above doesn't work as well anymore after fastify v3.
I'd be interested to hear from maintainers their recommended approach for not "hacking it in". Fastify has a large enough presence that I think support isn't too much to ask - a community driven plugin is all it would take. I'd be happy to help. What would be the recommended way to map the data sapper middleware needs from the request / response objects without them being 100% dependent on the express style api. Is there somewhere we could pass a function to just "pull out" the properties sapper is expecting? That way it could be more dynamic. From there it would be really easy to make a plugin of sorts - ie fastify-sapper or something similar.
I haven't look too closely, but perhaps you could use something like https://github.com/fastify/middie ?
I think there's some overlap between this issue and the issue of supporting serverless platforms. We're working on creating a list of the major issues we want to solve and plan to create a tracking issue organizing all the major issues like this in the coming weeks so that we know which problems are related and organize the discussion.
oh cool, thanks for the pointer. fwiw to anyone else who gets to this issue - the following code works with fastify v3 for the starter:
import sirv from 'sirv';
import compression from 'compression';
import * as sapper from '@sapper/server';
import midde from 'middie';
import fastify from 'fastify';
const app = fastify();
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';
(async () => {
app.get('/api/health', (req, res) => {
return { ok: true, timestamp: new Date().toISOString() }
})
await app.register(midde); // You can also use Express
app.use(compression({ threshold: 0 }));
app.use(sirv('static', { dev }));
const sap = sapper.middleware();
app.route({
method: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'PATCH', 'POST', 'DELETE'],
url: '/*',
handler: async (request, reply) => {
let next = (err) => {
if (err) {
throw err;
} else {
reply.status(404).send();
}
};
request.originalUrl = request.url;
reply.end = reply.send.bind(reply);
reply.setHeader = reply.header.bind(reply);
sap(request, reply, next);
return reply;
},
});
app.listen(PORT, err => {
if (err) console.log('error', err);
});
})();
EDIT: It doesn't work with the blog route still. I'm not familiar enough with svelte to understand why it needs to sometimes call .writeHead and what all the this.fetch and this.error are doing. That being said, if I go into the blog.js files and everywhere it uses the "express" compatible api change it to use the fastify compatible one, it works. See --
Change [slug].json.js
import posts from './_posts.js';
const lookup = new Map();
posts.forEach(post => {
lookup.set(post.slug, JSON.stringify(post));
});
export function get(req, res, next) {
// the `slug` parameter is available because
// this file is called [slug].json.js
const { slug } = req.params;
if (lookup.has(slug)) {
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(lookup.get(slug));
} else {
res.writeHead(404, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `Not found`
}));
}
}
to
import posts from './_posts.js';
const lookup = new Map();
posts.forEach(post => {
lookup.set(post.slug, JSON.stringify(post));
});
export function get(req, res, next) {
// the `slug` parameter is available because
// this file is called [slug].json.js
const { slug } = req.params;
if (lookup.has(slug)) {
res.header('Content-Type','application/json');
res.end(lookup.get(slug));
} else {
res.header('Content-Type','application/json');
res.end(JSON.stringify({
message: `Not found`
}));
}
}
Glad that worked! It sounds like there's a solution for this issue, so I'll go ahead and close it
Most helpful comment
oh cool, thanks for the pointer. fwiw to anyone else who gets to this issue - the following code works with fastify v3 for the starter:
EDIT: It doesn't work with the blog route still. I'm not familiar enough with svelte to understand why it needs to sometimes call
.writeHeadand what all thethis.fetchandthis.errorare doing. That being said, if I go into the blog.js files and everywhere it uses the "express" compatible api change it to use the fastify compatible one, it works. See --Change
[slug].json.jsto