I am using fastify with next.js and I need to include tracing (requestId is the problem so far). What I am doing right now is creating a fastify onRequest hook and generating a requestId value and setting it in request object (could be as a request header as well). What I want is to get access to this request object for two reasons:
In logger object (pino in this case, I want to include the requestId in all custom server-side logs).
In all request that needs to be made to other services need to include the requestId in headers.
Maybe I am missing something trivial and I'm not doing it the best way.
HERE SOME SNIPPETS
This how I am generating the reqId
const fastify = fastifyFactory({
logger, // logger configuration (Pino instance with custom configuration, see below)
genReqId: () => {
return Math.random()
.toString(36)
.slice(-6);
}
});
pino instance
const pino = require('pino');
const logger = pino({
messageKey: 'message',
prettyPrint: true,
changeLevelName: 'severity',
useLevelLabels: true,
base: {
serviceContext: {
service: 'web'
}
},
level:'info'
});
module.exports = {
logger
};
This is a plugin to gets the reqId generated and setting it to a query property within request object
const tracing = function tracing(fastify, opt, next) {
fastify.addHook('onRequest', (req, res, nextRequest) => {
const { id } = req;
const logger = fastify.log.child({ reqId: id });
req.query.reqId = id;
fastify.log = logger; //overrides the current fastify logger to include the reqId in all custom logs
nextRequest();
});
next();
};
tracing[Symbol.for('skip-override')] = true;
module.exports = tracing;
I have no problem when using fastify.log.info(...) because how logger is overrided in each request, it will include the reqId as a child log. The problem is that I want to create a generic logger to use at any part and fastify logger is not available in React components (for example to write logs at getInitialProps). Another important think is tha I need to include this reqId in all request I send to other services (ex: when fetching data), this is why I tried to store this value in request object but need to get it.
Starting from a project build with:
npx create-next-app --example custom-server-fastify custom-server-fastify-app
And changing the server.js with:
const Next = require('next')
const Fastify = require('fastify')
// your pino config
const fastify = Fastify({
logger: {
level: 'info',
prettyPrint: true,
changeLevelName: 'severity',
useLevelLabels: true,
base: {
serviceContext: {
service: 'web'
}
}
},
genReqId: () => { return Math.random().toString(36).slice(-6) }
})
// your plugin
const aPlugin = function yourPlugin (fastify, opts, next) {
fastify.addHook('onRequest', (request, reply, next) => {
request.log.info('hello')
const { id } = request
request.query.reqId = id
next()
})
next()
}
aPlugin[Symbol.for('skip-override')] = true
fastify.register(aPlugin)
[.... other generated code]
const port = parseInt(process.env.PORT, 10) || 3000
[.... other generated code]
fastify.get('/*', (req, reply) => {
console.log('-------->', req.id, req.query.reqId) // both your id is ok
return app.handleRequest(req.req, reply.res).then(() => {
reply.sent = true
})
[.... other generated code]
})
Then:
npm run dev
# another console
curl http://localhost:3000/
It will print out:
[1558441374784] INFO : Server listening at http://127.0.0.1:3000
serviceContext: {
"service": "web"
}
> Ready on http://localhost:3000
[1558441405416] INFO : incoming request
serviceContext: {
"service": "web"
}
reqId: "2i810l"
req: {
"method": "GET",
"url": "/",
"hostname": "localhost:3000",
"remoteAddress": "127.0.0.1",
"remotePort": 57863
}
req id ----> 2i810l
--------> 2i810l 2i810l
[ event ] build page: /
[ wait ] compiling ...
[1558441406171] INFO : request completed
serviceContext: {
"service": "web"
}
reqId: "2i810l"
res: {
"statusCode": 200
}
responseTime: 753.012099981308
So I think the misunderstanding is in the request object that is the Fastify request and not the Node.js "low level" request object, that could be accessed with request.req.
Moreover, running fastify.log = logger; is dangerous because it means that each request override and create a new logger and change the logger for the fastify instance, this is not safe, and as shown it is not necessary.
If you want more child logger (per route prefix per example) I suggest exploring/using the onRegister hook.
EDIT:
Now the custom hook print:
[1558443540483] INFO : hello
serviceContext: {
"service": "web"
}
reqId: "zjuhw2"
Related
I use next.js middleware to retrieve a data stored inside a cookie, and to check in a db (using strapi) if this specific user exists, or if he needs to register before going further.
// middleware.js
import { getToken } from 'next-auth/jwt';
import qs from 'qs';
import { MY_DB } from './constants';
export async function middleware(request) {
const token = await getToken({
req: request,
secret: process.env.SECRET,
});
const params = qs.stringify({
filters: {
address: {
$eq: token.sub,
},
},
});
const url = MY_DB + '/api/users/?' + params;
const result = await fetch(url, {
method: 'GET',
headers: { accept: 'application/json' },
});
// remaining code checks if the request is empty or not and returns the appropriate page
(...)
building my project returns the following error :
Failed to compile.
./node_modules/.pnpm/function-bind#1.1.1/node_modules/function-bind/implementation.js
Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation
Import trace for requested module:
./node_modules/.pnpm/function-bind#1.1.1/node_modules/function-bind/implementation.js
./node_modules/.pnpm/function-bind#1.1.1/node_modules/function-bind/index.js
./node_modules/.pnpm/get-intrinsic#1.1.3/node_modules/get-intrinsic/index.js
./node_modules/.pnpm/side-channel#1.0.4/node_modules/side-channel/index.js
./node_modules/.pnpm/qs#6.11.0/node_modules/qs/lib/stringify.js
./node_modules/.pnpm/qs#6.11.0/node_modules/qs/lib/index.js
> Build failed because of webpack errors
ELIFECYCLE Command failed with exit code 1.
I highly suspect the qs.stringify call given the stacktrace, but how can I overcome this in an elegant way ?
I come from a land of ASP.NET Core. Having fun learning a completely new stack.
I'm used to being able to:
name a route "orders"
give it a path like /customer-orders/{id}
register it
use the routing system to build a URL for my named route
An example of (4) might be to pass a routeName and then routeValues which is an object like { id = 193, x = "y" } and the routing system can figure out the URL /customer-orders/193?x=y - notice how it just appends extraneous key-vals as params.
Can I do something like this in oak on Deno?? Thanks.
Update: I am looking into some functions on the underlying regexp tool the routing system uses. It doesn't seem right that this often used feature should be so hard/undiscoverable/inaccessible.
https://github.com/pillarjs/path-to-regexp#compile-reverse-path-to-regexp
I'm not exactly sure what you mean by "building" a URL, but the URL associated to the incoming request is defined by the requesting client, and is available in each middleware callback function's context parameter at context.request.url as an instance of the URL class.
The documentation provides some examples of using a router and the middleware callback functions that are associated to routes in Oak.
Here's an example module which demonstrates accessing the URL-related data in a request:
so-74635313.ts:
import { Application, Router } from "https://deno.land/x/oak#v11.1.0/mod.ts";
const router = new Router({ prefix: "/customer-orders" });
router.get("/:id", async (ctx, next) => {
// An instance of the URL class:
const { url } = ctx.request;
// An instance of the URLSearchParams class:
const { searchParams } = url;
// A string:
const { id } = ctx.params;
const serializableObject = {
id,
// Iterate all the [key, value] entries and collect into an array:
searchParams: [...searchParams.entries()],
// A string representation of the full request URL:
url: url.href,
};
// Respond with the object as JSON data:
ctx.response.body = serializableObject;
ctx.response.type = "application/json";
// Log the object to the console:
console.log(serializableObject);
await next();
});
const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());
function printStartupMessage({ hostname, port, secure }: {
hostname: string;
port: number;
secure?: boolean;
}): void {
if (!hostname || hostname === "0.0.0.0") hostname = "localhost";
const address =
new URL(`http${secure ? "s" : ""}://${hostname}:${port}/`).href;
console.log(`Listening at ${address}`);
console.log("Use ctrl+c to stop");
}
app.addEventListener("listen", printStartupMessage);
await app.listen({ port: 8000 });
In a terminal shell (I'll call it shell A), the program is started:
% deno run --allow-net so-74635313.ts
Listening at http://localhost:8000/
Use ctrl+c to stop
Then, in another shell (I'll call it shell B), a network request is sent to the server at the route described in your question — and the response body (JSON text) is printed below the command:
% curl 'http://localhost:8000/customer-orders/193?x=y'
{"id":"193","searchParams":[["x","y"]],"url":"http://localhost:8000/customer-orders/193?x=y"}
Back in shell A, the output of the console.log statement can be seen:
{
id: "193",
searchParams: [ [ "x", "y" ] ],
url: "http://localhost:8000/customer-orders/193?x=y"
}
ctrl + c is used to send an interrupt signal (SIGINT) to the deno process and stop the server.
I am fortunately working with a React developer today!
Between us, we've found the .url(routeName, ...) method on the Router instance and that does exactly what I need!
Here's the help for it:
/** Generate a URL pathname for a named route, interpolating the optional
* params provided. Also accepts an optional set of options. */
Here's it in use in context:
export const routes = new Router()
.get(
"get-test",
"/test",
handleGetTest,
);
function handleGetTest(context: Context) {
console.log(`The URL for the test route is: ${routes.url("get-test")}`);
}
// The URL for the test route is: /test
The firebase Sveltekit client app and server api use a google cloud run hosting container. This works fine when I use the cloud run url: https://app...-4ysldefc4nq-uc.a.run.app/
But when I use firebase rewriting the client works fine using: https://vc-ticker.web.app/... but receives 502 and 504 responses from the API service. The cloud run log does not show any errors, receives the client fetch POST request and returns a Readablestream response.
But this API service response stream never arrives when using rewrites.
firebase.json
{
"hosting": {
"public": "public", !! NOT used, cloud run hosts the app
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"run": {
"serviceId": "vc-ticker-app",
"region": "us-central1"
}
}
]
}
}
+page.svelte client API request:
const logging = true;
const controller = new AbortController();
let reader = null;
const signal = controller.signal;
async function streamer(params) {
console.log("stream with logging:", logging, JSON.stringify(params));
try {
const response = await fetch("api/my-ticker", {
method: "POST",
body: JSON.stringify(params),
headers: {
"content-type": "application/json",
},
signal: signal,
});
const stream = response.body.pipeThrough(new TextDecoderStream("utf-8"));
reader = stream.getReader();
while (true) {
const { value, done } = await reader.read();
if (done || response.status !== 200) {
console.log("done response", response.status, done, value);
await reader.cancel(`reader done or invalid response: ${response.status}`);
reader = null;
break;
}
// response ok: parse multi json chunks => array => set store
const quotes = {};
JSON.parse(`[${value.replaceAll("}{", "},{")}]`).forEach((each, idx) => {
quotes[each.id] = [each.price, each.changePercent];
console.log(`quote-${idx}:`, quotes[each.id]);
});
positions.set(quotes);
}
} catch (err) {
console.log("streamer exception", err.name, err);
if (reader) {
await reader.cancel(`client exception: ${err.name}`);
reader = null;
}
}
}
$: if ($portfolio?.coins) {
const params = {
logging,
symbols: Object.values($portfolio.symbols),
};
streamer(params);
}
onDestroy(async () => {
if (reader) await reader.cancel("client destroyed");
controller.abort();
console.log("finished");
});
I use the Sveltekit adapter-node to build the app.
With rewrite rules, you can direct requests that match specific patterns to a single destination.Check your firebase.json file and verify if the rewrite configuration in the hosting section has the redirect serviceId name same as that from the deployed container image,as per below example
"hosting": {// ...
// Add the "rewrites" attribute within "hosting"
"rewrites": [ {
"source": "/helloworld",
"run": {
"serviceId": "helloworld", // "service name" (from when you [deployed the container image][3])
"region": "us-central1" // optional (if omitted, default is us-central1)
}
} ]
}
It is important to note that Firebase Hosting is subject to a 60-second request timeout. If your app requires more than 60 seconds to run, you'll receive an HTTPS status code 504 (request timeout). To support dynamic content that requires longer compute time, consider using an App Engine flexible environment.
You should also check the Hosting configuration page for more details about rewrite rules. You can also learn about the priority order of responses for various Hosting configurations.
I made it work with an external link to the cloud run api service (cors).
But I still do not understand why It can't be done without cors using only firebase rewrites.
+page.svelte client API request update:
Now using GET and an auth token to verify the api request on the endpoint server
const search = new URLSearchParams(params);
const apiHost = "https://fs-por....-app-4y...q-uc.a.run.app/api/yahoo-finance-streamer";
const response = await fetch(`${apiHost}?${search.toString()}`, {
method: "GET",
headers: {
"auth-token": await getIdToken(),
},
signal: signal,
});
And a handle hook to verify the auth token and handle cors:
const logging = true;
const reqUnauthorized = { status: 403, statusText: 'Unauthorized!' };
/** #type {import('#sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
let response;
if (event.request.method !== "OPTIONS") {
if (event.url.pathname.startsWith('/api')) {
const authToken = event.request.headers.get("auth-token")
const { error = null, decodedToken } = await decodeIdToken(logging, authToken)
if (error) return new Response(error.message, reqUnauthorized);
if (verifyUser(logging, decodedToken) === false) {
return new Response(`user auth failed for: ${decodedToken.email}`, reqUnauthorized);
}
}
response = await resolve(event);
} else { // handle cors preflight OPTIONS
response = new Response("", { status: 200 });
}
response.headers.append('Access-Control-Allow-Headers', "*");
response.headers.append('Access-Control-Allow-Origin', "*");
return response;
}
From firebase support:
I got an answer from the engineering team. Unfortunately Firebase Hosting does not support streaming responses at the moment. I’ve created a feature request so they will consider implementing it.
Please be informed that submitting a feature request doesn’t guarantee that it will be implemented. Keep an eye on the release notes.
I realize that this is not the answer you expected from me, but unfortunately there is nothing I can do about it.
I want to add a fetch call to the initial load of index.html on a local app. This data is otherwise loaded server-side, but for this particular case I need to make an http call in dev.
I am having difficulty proxying the webpack-dev-server with the fetch added to the DOM.
The proxy is working correctly after my app is instantiated and I use Axios to make http calls to /api, but on init, the proxy is still serving from localhost instead of the target endpoint.
This is a bit puzzling to me - why might the proxy work in JS post-load but not on init?
devServer: {
contentBase: '/some/path',
port: 9000,
https: true,
open: true,
compress: true,
hot: true,
proxy: { '/api/*': [Object] }
},
Script in index
<script>
(async function() {
const data = await getData();
addDataset(data);
async function getData() {
return fetch('/api/my/endpoint').then(
(response) => response.data.result,
);
}
function addDataset(data) {
var el = document.getElementById('root');
const parsed = JSON.parse(data);
Object.entries(parsed).forEach((entry) => {
const [k, val] = entry;
el.dataset[k] = JSON.stringify(val);
});
}
})();
</script>
Error
400 Bad request
Request URL - https://localhost:9000/api/my/endpoint
I'm having trouble using the meteor slingshot component with the S3 with temporary AWS Credentials component. I keep getting the error Exception while invoking method 'slingshot/uploadRequest' InvalidClientTokenId: The security token included in the request is invalid.
Absolutely no idea what I'm doing wrong. If I use slingshot normally without credentials it works fine.
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
const cryptoRandomString = require('crypto-random-string');
var AWS = require('aws-sdk');
var sts = new AWS.STS();
Slingshot.createDirective('UserProfileResumeUpload', Slingshot.S3Storage.TempCredentials, {
bucket: 'mybuckname', // change this to your s3's bucket name
region: 'ap-southeast-2',
acl: 'private',
temporaryCredentials: Meteor.wrapAsync(function (expire, callback) {
//AWS dictates that the minimum duration must be 900 seconds:
var duration = Math.max(Math.round(expire / 1000), 900);
sts.getSessionToken({
DurationSeconds: duration
}, function (error, result) {
callback(error, result && result.Credentials);
});
}),
authorize: function () {
//Deny uploads if user is not logged in.
if (!this.userId) {
const message = 'Please login before posting files';
throw new Meteor.Error('Login Required', message);
}
return true;
},
key: function () {
return 'mydirectory' + '/' + cryptoRandomString(10) + moment().valueOf();
}
});
Path: Settings.json
{
"AWSAccessKeyId": "myAWSKEYID",
"AWSSecretAccessKey": "MyAWSSeceretAccessKey"
}
I've done it in server side like this :
Slingshot.createDirective("UserProfileResumeUpload", Slingshot.S3Storage, {
AWSAccessKeyId: Meteor.settings.AWS.AccessKeyId,
AWSSecretAccessKey: Meteor.settings.AWS.SecretAccessKey,
bucket: 'mybuckname', // change this to your s3's bucket name
region: 'ap-southeast-2',
acl: 'private',
...
}
and in settings.json
{
"AWS": {
"AccessKeyId": "myAWSKEYID",
"SecretAccessKey": "MyAWSSeceretAccessKey"
}
}