I'm trying to update multiple collections in my Firestore and RTDB by triggering Google cloud function through an HTTP request, and I've done some several times of testing, but the batch commit works from time to time. I've also tried running test excluding RTDB update from the code, but nothing changes much.
If something goes wrong, I get the following error message with status code 500.
Error: Cannot modify a WriteBatch that has been committed.
Here's the example code:
Server-side code
'use strict';
module.exports = ({ admin, cors, express, functions }) => {
const app = express();
const fireStore = admin.firestore();
const rtdb = admin.database();
const apps = fireStore.collection('apps');
const users = fireStore.collection('users');
const batch = admin.firestore().batch();
app.use(cors({ origin: true }));
...
app.post('/', (req, res) => {
const uid = req.user.user_id;
const data = req.body;
const appsRef = apps.doc(uid);
const usersRef = users.doc(uid);
const activityState = {
currentActiveStatus: data.activityState.currentActiveStatus,
usingApp: data.activityState.usingApp
};
const appState = {
emailVerified: data.user.emailVerified
};
const userState = {
displayName: data.user.displayName,
photoURL: data.user.photoURL,
currentActiveStatus: data.user.currentActiveStatus,
lastLoginAt: admin.firestore.FieldValue.serverTimestamp()
};
batch.update(appsRef, appState);
batch.update(usersRef, userState);
return batch.commit().then(() => {
console.log('Batch commit finished!');
return admin.database().ref(`status/${uid}`).update(activityState).then(() => {
res.status(201).send({ message: 'Successfully Initialize Default State' });
});
}).catch(err => console.log('Err:', err));
});
return functions.https.onRequest(app);
};
Client-side code
const data = {
activityState: {
currentActiveStatus: "online",
usingApp: "true"
},
user: {
displayName: this.displayName,
photoURL: this.photoURL,
currentActiveStatus: "online",
emailVerified: "true"
}
};
this.userService.updateUserProfile(this.displayName, this.photoURL).then((accessToken) => {
const url = 'https://us-central1/dbname/cloudfunctions.net/functionname';
this.http.post(url, JSON.stringify(data), {
headers: {'Authorization': accessToken, 'Content-Type': 'application/json; charset=utf-8'}
}).subscribe((res) => {
// Worked well
}, (err) => {
// Went wrong
});
});
Error message in details
Error: Cannot modify a WriteBatch that has been committed.
at WriteBatch.verifyNotCommitted (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/firestore/src/write-batch.js:148:13)
at WriteBatch.update (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/firestore/src/write-batch.js:333:10)
at app.post (/user_code/exports/auth/user/startapp/initDefaultState.f.js:54:11)
at Layer.handle [as handle_request] (/user_code/node_modules/express/lib/router/layer.js:95:5)
at next (/user_code/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/user_code/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/user_code/node_modules/express/lib/router/layer.js:95:5)
at /user_code/node_modules/express/lib/router/index.js:281:22
at Function.process_params (/user_code/node_modules/express/lib/router/index.js:335:12)
at next (/user_code/node_modules/express/lib/router/index.js:275:10)
Perhaps I'm missing something out in my code?
I resolved my problem. It was a silly mistake that I've made.
I had to declare batch inside app.post().
app.post('/', (req, res) => {
const batch = admin.firestore().batch();
});
Instead
module.exports = ({ admin, cors, express, functions }) => {
const app = express();
const fireStore = admin.firestore();
const rtdb = admin.database();
const apps = fireStore.collection('apps');
const users = fireStore.collection('users');
const batch = admin.firestore().batch();
};
Related
While creating matrixClient by using matrix-js-sdk, I am getting an error of TypeError: this.opts.request is not a function.
The error is at line no. 19. but don't understand the reason.
const express = require('express');
const sdk = require('matrix-js-sdk');
const app = express();
const port = 3000;
require('dotenv').config();
const BASE_URL = process.env.BASE_URL;
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
const USER_ID = process.env.USER_ID;
const PASSWORD = process.env.PASSWORD;
const matrixClient = sdk.createClient({
baseUrl: BASE_URL,
accessToken: ACCESS_TOKEN,
userId: `#${USER_ID}:matrix.org`,
});
app.get('/', async (req, res) => {
await matrixClient.startClient(); // error
matrixClient.once('sync', function (state, prevState, res) {
console.log(state); // state will be 'PREPARED' when the client is ready to use
});
res.send('hello');
});
// getAccessToken(USER_ID, PASSWORD);
function getAccessToken(userId, password) {
const client = sdk.createClient('https://matrix.org');
client
.login('m.login.password', { user: userId, password: password })
.then((response) => {
console.log(response.access_token);
})
.catch((err) => {
console.log('access_token error :', err);
});
}
app.listen(port, () => {
console.log(`app is listening at http://localhost:${port}`);
});
ERROR :
app is listening at http://localhost:3000
Getting saved sync token...
Getting push rules...
Attempting to send queued to-device messages
Got saved sync token
Got reply from saved sync, exists? false
All queued to-device messages sent
Getting push rules failed TypeError: this.opts.request is not a function
at MatrixHttpApi.doRequest (A:\matrix\matrix_node\node_modules\matrix-js-sdk\lib\http-api.js:741:23)
at MatrixHttpApi.requestOtherUrl (A:\matrix\matrix_node\node_modules\matrix-js-sdk\lib\http-api.js:620:17)
at MatrixHttpApi.request (A:\matrix\matrix_node\node_modules\matrix-js-sdk\lib\http-api.js:576:17)
at MatrixHttpApi.authedRequest (A:\matrix\matrix_node\node_modules\matrix-js-sdk\lib\http-api.js:524:33)
ix-js-sdk\lib\client.js:7283:22) at SyncApi.getPushRules (A:\matrix\matrix_node\node_modules\matrix-js-sdk\lib\sync.js:155:42) at SyncApi.sync (A:\matrix\matrix_node\node_modules\matrix-js-sdk\lib\sync.js:674:16)
at MatrixClient.startClient (A:\matrix\matrix_node\node_modules\matrix-js-sdk\lib\client.js:497:18) at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async A:\matrix\matrix_node\index.js:19:3
Waiting for saved sync before retrying push rules...
I found a nice npm package by the name of Express Async Errors which according to the documentation, it's really nice to use.
However, if I implement it, the server will crash.
Here is my Route handler code
Controller
const { Genre } = require("../models");
const { StatusCodes } = require("http-status-codes");
const getGenre = async (req, res) => {
const genre = await Genre.findById({ _id: req.params.id });
if (!genre) {
return res.status(StatusCodes.BAD_REQUEST).json({
message: "The genre with the given ID was not found.",
});
}
res.status(StatusCodes.OK).json({ status: "success", data: genre });
};
*router*
const express = require("express");
const { authenticate, admin } = require("../middleware");
const router = express.Router();
const { schemaValidator } = require("../middleware");
const validateRequest = schemaValidator(true);
const { genres } = require("../controllers");
const { getAllGenres, getGenre, createGenre, updateGenre, deleteGenre } =
genres;
.route("/genres")
.get(getAllGenres)
Main Application Entry
require("express-async-errors");
//Routers
const routers = require("./router");
const connectDB = require("./DB/connect");
const express = require("express");
const app = express();
app.use(config.get("URI"), routers);
app.use(notFoundMiddleware);
const start = async () => {
const port = process.env.PORT || 3000;
const connectionString = config.get("mongoDB.connString");
await connectDB(connectionString)
.then(() => DBdebug(`Connected to MongoDB: ${connectionString}`))
.catch(() => console.log("MongoDB connection failure"));
app.listen(port, () => debug(`Listening on port ${port}...`));
};
start();
Above code is imported into index.js together with express-async-errors.
According to the document, if I create an error, express-async-errors has to handle this without crashing the application. My question is what I'm doind wrong???
I shut down the Mongo-driver just to create a scenario that the server is down with a status 503.
MongooseServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017
at Function.Model.$wrapCallback (/Volumes/Seagate/lib/model.js:5087:32)
at /Volumes/Seagate/lib/query.js:4510:21
at /Volumes/Seagate/node_modules/mongoose/lib/helpers/promiseOrCallback.js:32:5
From previous event:
at promiseOrCallback (/Volumes/Seagate/node_modules/mongoose/lib/helpers/promiseOrCallback.js:31:10)
at model.Query.exec (/Volumes/Seagate/node_modules/mongoose/lib/query.js:4509:10)
at model.Query.Query.then (/Volumes/Seagate/node_modules/mongoose/lib/query.js:4592:15)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
Instead of above error, I should see "Something went wrong" as the default message in Postman without crashing the application.
Can someone point me to the right direction?
NB: The link which I had used us is https://www.npmjs.com/package/express-async-errors
Thank you very much in advanced.
After two days of researching my problem, I finally convinced my self that the problem was me and found a solution for this particular matter.
I have created an ErrorHandlerMiddleware and in the particular middleware I check if the error is an instance of MongooseError object, if this is the case, I just send for now an custom message.
See code below:
const { StatusCodes } = require("http-status-codes");
const { CustomApiError } = require("../errors");
const Mongoose = require("mongoose");
const errorHandlerMiddleware = (err, req, res, next) => {
console.log("errorMiddleWare");
if (err instanceof CustomApiError) {
return res.status(err.statusCode).json({ message: err.message });
}
if (err instanceof Mongoose.Error) {
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: "There is a technical problem, please try again later",
});
}
};
module.exports = { errorHandlerMiddleware };
In main application entry, I just pass errorHandlerMiddleware as a argument reference in app.use.
See code below
require("express-async-errors");
//Routers
const routers = require("./router");
const connectDB = require("./DB/connect");
const express = require("express");
const app = express();
app.use(config.get("URI"), routers);
app.use(notFoundMiddleware);
app.use(errorHandlerMiddleware);
const start = async () => {
const port = process.env.PORT || 3000;
const connectionString = config.get("mongoDB.connString");
await connectDB(connectionString)
.then(() => DBdebug(`Connected to MongoDB: ${connectionString}`))
.catch(() => console.log("MongoDB connection failure"));
app.listen(port, () => debug(`Listening on port ${port}...`));
};
start();
And this is the final result in PostMan:
If there are any comments regarding this solution, feel free to do this.
By comments and can learn more!!!
Thank you in advanced and keep coding!!!!
so I get this error every time I re-render the site
" GET http://localhost:3000/api/ShopItems 431 (Request Header Fields Too Large)"
I can see that my fetch doesn't go as planed and invokes the error above ⬆️
The chrome developer tool points me to the "ShopItemsActions" fill
import { FETCH_SHOPITEMS } from "../types"
export const fetchShopItems = () => async (dispatch) => {
const res = await fetch("/api/ShopItems");
const data = res.json();
console.log(data);
dispatch({
type: FETCH_SHOPITEMS,
payload: data
});
}
The chrome developer tool mark the third line "const res = await fetch("/api/ShopItems")"
Question: how can i fix this error?
Edit:
the server side code
const express = require("express");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");
const shortid = require('shortid');
const app = express();
app.use(bodyParser.json());
mongoose.connect("mongodb://localhost/bread_project_db", {
useNewUrlParser:true,
useCreateIndex: true,
useUnifiedTopology:true,
});
const ShopItem = mongoose.model(
"ShopItems",
new mongoose.Schema({
id: { type: String, default: shortid.generate },
name: String,
image: String,
price: Number,
info: String,
flourType: [String],
}))
app.get("/api/ShopItems", async (req, res)=>{
const ShopItems = await ShopItem.find({});
res.send(ShopItems);
});
app.post("/api/ShopItems", async (req, res)=>{
const newShopItem = new ShopItem(req.body);
const savedShopItem = await newShopItem.save();
res.send(savedShopItem);
});
app.delete("/api/ShopItems/:id", async (req, res) => {
const deletedShopItem = await ShopItem.findByIdAndDelete(req.params.id);
res.send(deletedShopItem);
});
const port = process.env.PORT || 5000;
app.listen( port, () => console.log(`server at http://localhost:${port}`));
I am using postman and I dont have cURL installed for some reason.
the postman just show this:
431 Request Header Fields Too Large
The server is unwilling to process the request because either an individual header field, or all the header fields collectively, are too large. Proposed in an Internet-Draft.
I'm very new to testing and I can't figure out how to test this scenario. I use supertest and Jest for testing. In my Express app I use jwt authentication. I store a jwt token that identifies a user in a http only cookie, and it's been created when a user creates and account or logs in.
Now, I have a route that is responsible for a password change requested by a user:
router.post('/reset-password', async function (req, res, next) {
try {
//relevant to the question part here
//
const { userToken } = req.cookies
if (!userToken) {
res.status(401).json({ error: 'unauthorized' })
return
}
const { email } = jwt.verify(userToken, process.env.JWT_SECRET)
/////////////////////
} catch (err) {
res.status(401).json({ error: 'unable to verify' })
}
})
As I understand, in order to test this function, I need to set a cookie in beforeAll. I tried doing so by registering a user that normaly sets required token. Here is my test code:
const request = require('supertest')
const app = require('../app')
const MongoClient = require('mongodb').MongoClient
const client = new MongoClient(`mongodb://localhost:27017/img-test`, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
beforeAll(async () => {
await client.connect()
app.locals.db = client.db('img-test')
await request(app).post('/register').send({
username: 'Bob',
email: 'bob#email.com',
password: 'Hardpass0!',
checkboxTerms: 'on',
'g-recaptcha-response': 'asdasd',
})
})
describe('Password reset', () => {
test('Should reset password', async () => {
await request(app)
.post('/api/account/reset-password')
.send({
passwordCurrent: 'Hardpass0!',
passwordNew: 'Hardpass1!',
})
.expect(200)
})
})
afterAll(async () => {
let db = app.locals.db
await db.dropDatabase()
client.close()
})
And of course, it fails. userToken is going to be undefined because as I understand supertest is not setting an http only cookie when a user is registered, and there is no token on req.cookies? How do I test this scenario? What am I doing wrong? Thanks for help.
I have read through several posts regarding this same issue but I cannot figure out where I am initially sending the headers. Here is the stack trace:
Also seems weird that I am getting a 204 as it adds to the db, and then it spits out a 404. Clearly something is wrong, but i'm simply not seeing it.
I've tried adding returns to every res.json() statement.
OPTIONS /api/users/favorites 204 1.822 ms - 0
PATCH /api/users/favorites 404 19.769 ms - 160
Unhandled rejection Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:455:11)
at ServerResponse.header (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/response.js:771:10)
at ServerResponse.send (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/response.js:170:12)
at ServerResponse.json (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/response.js:267:15)
at /Users/beers/projects/node/cryptopal-api/src/users/usersRouter.js:30:38
From previous event:
at Builder.Target.then (/Users/beers/projects/node/cryptopal-api/node_modules/knex/lib/interface.js:27:24)
at /Users/beers/projects/node/cryptopal-api/src/users/usersRouter.js:19:8
at Layer.handle [as handle_request] (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/layer.js:95:5)
at next (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/route.js:137:13)
at requireAuth (/Users/beers/projects/node/cryptopal-api/src/middleware/jwt-auth.js:31:5)
at Layer.handle [as handle_request] (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/layer.js:95:5)
at next (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/layer.js:95:5)
at /Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/index.js:281:22
at Function.process_params (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/index.js:335:12)
at next (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/index.js:275:10)
at Function.handle (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/index.js:174:3)
at router (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/index.js:47:12)
at Layer.handle [as handle_request] (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/index.js:317:13)
at /Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/index.js:284:7
at Function.process_params (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/index.js:335:12)
at next (/Users/beers/projects/node/cryptopal-api/node_modules/express/lib/router/index.js:275:10)
at /Users/beers/projects/node/cryptopal-api/node_modules/body-parser/lib/read.js:130:5
at invokeCallback (/Users/beers/projects/node/cryptopal-api/node_modules/raw-body/index.js:224:16)
at done (/Users/beers/projects/node/cryptopal-api/node_modules/raw-body/index.js:213:7)
at IncomingMessage.onEnd (/Users/beers/projects/node/cryptopal-api/node_modules/raw-body/index.js:273:7)
at IncomingMessage.emit (events.js:205:15)
at endReadableNT (_stream_readable.js:1154:12)
here is my usersRouter.js
require('dotenv').config();
const express = require('express');
// const rp = require('request-promise');
const usersRouter = express.Router()
const jsonParser = express.json()
const UsersService = require('./usersService.js')
const { requireAuth } = require('../middleware/jwt-auth.js')
usersRouter
.patch('/favorites', requireAuth, (req,res,next) => {
const db = req.app.get('db');
const { coinID } = req.body;
const { user_id } = req;
console.log(res.headersSent) // EQUALS FALSE
// get current favorites for user to see if it already exists in db
UsersService.getUserFavorites(db, user_id)
.then( response => {
console.log(res.headersSent) // EQUALS TRUE
let favExists = false;
response.favorites.forEach( fav => {
if(fav == coinID)
favExists = true;
})
if(favExists){
return res.status(401).json({ error: "Coin already exists in favorites" })
}else{
UsersService.addToUserFavorites(db, user_id, coinID)
.then( response => {
return res.status(204).json({ response })
})
}
})
next()
});
module.exports = usersRouter;
As you can see, the patch route calls a middleware function requireAuth to authenticate a user before it will add a favorite.
Here is that file jwt-auth.js
const AuthService = require('../auth/authService.js')
function requireAuth(req, res, next) {
const authToken = req.get('Authorization') || ''
let bearerToken
if (!authToken.toLowerCase().startsWith('bearer ')) {
return res.status(401).json({ error: 'Missing bearer token' })
} else {
bearerToken = authToken.slice(7, authToken.length)
}
try {
const payload = AuthService.verifyJwt(bearerToken);
AuthService.getUserByEmail(
req.app.get('db'),
payload.sub,
)
.then(user => {
if (!user){
return res.status(401).json({ error: 'Unauthorized request' })
}
next();
})
.catch(err => {
console.error(err)
next(err)
})
req.user_id = payload.user_id;
next()
} catch(error) {
return res.status(401).json({ error: 'Unauthorized request' })
}
}
module.exports = {
requireAuth,
}
I will include the usersService.js and authService.js files as well because a couple functions are called within them, but I don't believe thats where the error lies.
usersService.js:
const xss = require('xss');
const config = require('../config.js');
const UsersService = {
getUserByID(db,id){
return db('cryptopal_users')
.where({ id })
.first()
},
getUserFavorites(db,id){
return db('cryptopal_users')
.where('id', id)
.first()
},
addToUserFavorites(db,id,favorites){
return db('cryptopal_users')
.where('id', id)
.update({
favorites: db.raw('array_append(favorites, ?)', [favorites])
})
},
}
module.exports = UsersService;
authService.js
const xss = require('xss');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const config = require('../config.js');
const AuthService = {
validatePassword(password){
if(password.length < 6){
return "Password must be at least 6 characters"
}
},
hashPassword(password){
return bcrypt.hash(password,12);
},
comparePasswords(password,hash){
return bcrypt.compare(password,hash);
},
createJwt(subject, payload) {
return jwt.sign(payload, config.JWT_SECRET, {
subject,
expiresIn: config.JWT_EXPIRY,
algorithm: 'HS256',
})
},
checkEmailUnique(db,email){
return db('cryptopal_users')
.where({ email })
.first()
.then(user => !!user)
},
insertUser(db,user){
return db
.insert(user)
.into('cryptopal_users')
.returning('*')
.then( ([user]) => user )
},
serializeUser(user){
return {
id: user.id,
name: xss(user.name),
email: xss(user.email),
date_created: new Date(user.date_created),
}
},
getUserByEmail(db,email){
return db('cryptopal_users')
.where({ email })
.first()
},
verifyJwt(token) {
return jwt.verify(token, config.JWT_SECRET, {
algorithms: ['HS256'],
})
},
}
module.exports = AuthService;
I believe the issue lies somewhere within the jwt-auth.js file, but not 100% sure. The code does get all the way through to the end and it inserts the favorite into the database after authenticating the users, but then throws an error about the headers.
the problem was that at the very end of the patch route, I had a next(). Once i removed that, it worked fine.