I have an axios interceptor for cases, where I need the user to be authorized, but he isn't. For example, because the token is expired.
Now, after a token refresh, the original request should be retried.
However, currently the original requests, seems to be changed, so that the Server gives me a JSON.parse error.
SyntaxError: Unexpected token " in JSON at position 0
at JSON.parse (<anonymous>)
at createStrictSyntaxError (/var/app/current/node_modules/body-parser/lib/types/json.js:158:10)
at parse (/var/app/current/node_modules/body-parser/lib/types/json.js:83:15)
at /var/app/current/node_modules/body-parser/lib/read.js:121:18
at invokeCallback (/var/app/current/node_modules/raw-body/index.js:224:16)
at done (/var/app/current/node_modules/raw-body/index.js:213:7)
at IncomingMessage.onEnd (/var/app/current/node_modules/raw-body/index.js:273:7)
at IncomingMessage.emit (events.js:314:20)
at IncomingMessage.EventEmitter.emit (domain.js:483:12)
at endReadableNT (_stream_readable.js:1241:12)
This is because, instead of the original request, that is JSON, it seems to process it again, puts it in quotes etc., so it becomes a string and the bodyparser, throws the error above.
So the request content, becomes:
"{\"traderaccount\":\"{\\\"traderaccountID\\\":\\\"undefined\\\",\\\"traderID\\\":\\\"2\\\",\\\"name\\\":\\\"Conscientious\\\",\\\"order\\\":99,\\\"myFxLink\\\":\\\"\\\",\\\"myFxWidget\\\":\\\"\\\",\\\"copyFxLink\\\":\\\"83809\\\",\\\"tokenLink\\\":\\\"\\\",\\\"tradertext\\\":{\\\"tradertextID\\\":\\\"\\\",\\\"traderaccountID\\\":\\\"\\\",\\\"language\\\":\\\"\\\",\\\"commission\\\":\\\"\\\",\\\"affiliateSystem\\\":\\\"\\\",\\\"leverage\\\":\\\"\\\",\\\"mode\\\":\\\"\\\",\\\"description\\\":\\\"\\\"},\\\"accountType\\\":\\\"\\\",\\\"accountTypeID\\\":1,\\\"minInvest\\\":2000,\\\"currency\\\":\\\"\\\",\\\"currencySymbol\\\":\\\"\\\",\\\"currencyID\\\":1,\\\"affiliateSystem\\\":1}\"}"
instead of
{"traderaccount":"{\"traderaccountID\":\"undefined\",\"traderID\":\"2\",\"name\":\"Conscientious\",\"order\":99,\"myFxLink\":\"\",\"myFxWidget\":\"\",\"copyFxLink\":\"83809\",\"tokenLink\":\"\",\"tradertext\":{\"tradertextID\":\"\",\"traderaccountID\":\"\",\"language\":\"\",\"commission\":\"\",\"affiliateSystem\":\"\",\"leverage\":\"\",\"mode\":\"\",\"description\":\"\"},\"accountType\":\"\",\"accountTypeID\":1,\"minInvest\":2000,\"currency\":\"\",\"currencySymbol\":\"\",\"currencyID\":1,\"affiliateSystem\":1}"}
from the original axios request content.
Both are the unformated request contents, that I can see in the developer network console.
The content type, is application/json in both cases.
Below is the Interceptor code:
Axios.interceptors.response.use(
(response) => {
return response;
},
(err) => {
const error = err.response;
if (
error !== undefined &&
error.status === 401 &&
error.config &&
!error.config.__isRetryRequest
) {
if (this.$store.state.refreshToken === "") {
return Promise.reject(error);
}
return this.getAuthToken().then(() => {
const request = error.config;
request.headers.Authorization =
Axios.defaults.headers.common[globals.AXIOSAuthorization];
request.__isRetryRequest = true;
return Axios.request(request);
});
}
return Promise.reject(error);
}
);
private getAuthToken() {
if (!this.currentRequest) {
this.currentRequest = this.$store.dispatch("refreshToken");
this.currentRequest.then(
this.resetAuthTokenRequest,
this.resetAuthTokenRequest
);
}
return this.currentRequest;
}
private resetAuthTokenRequest() {
this.currentRequest = null;
}
// store refreshToken
async refreshToken({ commit }) {
const userID = this.state.userID;
const refreshToken = Vue.prototype.$cookies.get("refreshToken");
this.commit("refreshLicense");
commit("authRequest");
try {
const resp = await axios.post(serverURL + "/refreshToken", {
userID,
refreshToken,
});
if (resp.status === 200) {
return;
} else if (resp.status === 201) {
const token = resp.data.newToken;
const newRefreshToken = resp.data.newRefreshToken;
Vue.$cookies.set(
"token",
token,
"14d",
undefined,
undefined,
process.env.NODE_ENV === "production",
"Strict"
);
Vue.$cookies.set(
"refreshToken",
newRefreshToken,
"30d",
undefined,
undefined,
process.env.NODE_ENV === "production",
"Strict"
);
axios.defaults.headers.common[globals.AXIOSAuthorization] = token;
commit("authSuccessRefresh", { newRefreshToken });
} else {
this.dispatch("logout");
router.push({
name: "login",
});
}
} catch (e) {
commit("authError");
this.dispatch("logout");
}
So, can you help me to prevent Axios on the retried request to change the request content. So it doesn't put it into quotes and quote the already exisitng quotes?
Thanks to the comment I found a solution.
Try to parse the content before resending it:
axios.interceptors.response.use(
(response) => response,
(error) => {
const status = error.response ? error.response.status : null;
if (status === 401 && error.config && !error.config.__isRetryRequest) {
return refreshToken(useStore()).then(() => {
const request = error.config;
request.headers.Authorization =
axios.defaults.headers.common["Authorization"];
request.__isRetryRequest = true;
try {
const o = JSON.parse(request.data);
if (o && typeof o === "object") {
request.data = o;
}
} catch (e) {
return axios.request(request);
}
return axios.request(request);
});
}
return Promise.reject(error);
});
Related
I'm trying to remove message notification. but this error appear "SyntaxError: Unexpected token S in JSON at position 0 at JSON.parse" , is there any relation with the type defined in the headers? How can i resolve this error?
Front-End Angular 12
public removeNotificationForUser(onlineUserModel: OnlineUserModel) {
let targetUsername = onlineUserModel.userName;
const url_ = `${this.baseUrlRemoveNotifications}?targetUsername=${targetUsername}`;
const url = this.baseUrlRemoveNotifications + "/" + targetUsername;
const body = {};
const options = {
headers: new HttpHeaders({
"Content-Type": "application/json",
"X-XSRF-TOKEN": this.cookieService.get("XSRF-TOKEN"),
}),
};
return this.http.patch<any>(url_, body, options).subscribe(
(val) => {
console.log("PATCH call successful value returned in body", val);
},
(response) => {
console.log("PATCH call in error", response);
},
() => {
console.log("The PATCH observable is now completed.");
}
);
}
Back-End Asp.net Core
[HttpPatch("[action]/{targetUsername}")]
[Route("removeNotifications")]
public async Task<IActionResult> RemoveNotifications( [FromQuery] string targetUsername)
{
try
{
var sender = await _userManager.FindByNameAsync(targetUsername);
var reciever = await _userManager.FindByNameAsync(User.Identity.Name);
// Find The Connection
var RecieverResetNotification= _userInfoInMemory.GetUserInfo(User.Identity.Name);
var SenderResetNotification = _userInfoInMemory.GetUserInfo(targetUsername);
var notificationToBeRemoved = _context.Notifications.Where(N => ((N.ReceiverId == reciever.Id) && (N.SenderId == sender.Id) && (N.IsSeen == false))).ToList();
//Send My reset notification to the the others sessions :
var mySessions = _context.Connections.Where(C => ((C.ConnectionID != RecieverResetNotification.ConnectionID) && (C.Username == reciever.UserName) && (C.Connected == true))).Select(MyCID => MyCID.ConnectionID).ToList();
if (mySessions != null)
{
await _hubContext.Clients.Clients(mySessions).SendAsync("MyResetNotification", RecieverResetNotification);
}
//Test if notificationToBeRemoved
if (notificationToBeRemoved != null)
{
notificationToBeRemoved.ForEach(NR => NR.IsSeen = true);
_context.SaveChanges();
}
// My Methode to update Notification Table => change Iseen column to true //
return Ok("Success");
}
catch (Exception ex)
{
Log.Error("An error occurred while seeding the database {Error} {StackTrace} {InnerException} {Source}",
ex.Message, ex.StackTrace, ex.InnerException, ex.Source);
}
return BadRequest("Failed");
}
This is part of the error message that I am getting:
[Error] ERROR – TypeError: undefined is not an object (evaluating '_co.user.username') TypeError: undefined is not an object (evaluating '_co.user.username')(anonymous function)checkAndUpdateView — core.js:44...
My login process works fine and data of the user is gotten fine, on ionic serve version of my app, but on ios I can see that error message, like json encoding doesn't work fine or something. Why is the JSON working fine on website, but not on the app? Here is content of TokenService :
constructor(private cookieService: CookieService) {}
setToken(token) {
this.cookieService.set("chat_token", token);
}
getToken() {
return this.cookieService.get("chat_token");
}
deleteToken() {
this.cookieService.delete("chat_token");
}
getPayload() {
const token = this.getToken();
let payload;
if (token) {
payload = token.split(".")[1];
payload = JSON.parse(window.atob(payload));
}
return payload.data;
}
and this is the loginUser function in LoginComponent , that is triggered on logging in:
loginUser() {
this.showSpinner = true;
this.authService.loginUser(this.loginForm.value).subscribe(
data => {
this.tokenService.setToken(data.token);
localStorage.setItem("currentUser", JSON.stringify(data));
this.loginForm.reset();
setTimeout(() => {
this.router.navigate(["/streams"]);
}, 200);
},
err => {
this.showSpinner = false;
if (err.error.message) {
this.errorMessage = err.error.message;
}
}
);
}
Now, the server side, I have this rout in routes/ directory, in node express in file authRoutes.js:
router.post('/login', AuthCtrl.LoginUser);
And then I have this in routes/ directory, in file userRoutes.js:
const express = require('express');
const router = express.Router();
const UserCtrl = require('../controllers/users');
const AuthHelper = require('../Helpers/AuthHelper');
router.get('/users', AuthHelper.VerifyToken, UserCtrl.GetAllUsers);
router.get('/user/:id', AuthHelper.VerifyToken, UserCtrl.GetUser);
router.get(
'/username/:username',
AuthHelper.VerifyToken,
UserCtrl.GetUserByName
);
router.post('/user/view-profile', AuthHelper.VerifyToken, UserCtrl.ProfileView);
router.post(
'/change-password',
AuthHelper.VerifyToken,
UserCtrl.ChangePassword
);
module.exports = router;
This is the part of controller auth.js on node server side:
async LoginUser(req, res) {
if (!req.body.username || !req.body.password) {
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ message: "No empty fields allowed" });
}
await User.findOne({ username: Helpers.firstUpper(req.body.username) })
.then(user => {
if (!user) {
return res.status(HttpStatus.NOT_FOUND).json({ message: "Username not found" });
}
return bcrypt.compare(req.body.password, user.password).then(result => {
if (!result) {
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ message: "Password is incorrect" });
}
const token = jwt.sign({ data: user }, dbConfig.secret, {
expiresIn: "5h"
});
res.cookie("auth", token);
return res.status(HttpStatus.OK).json({ message: "Login successful", user, token });
});
})
.catch(err => {
console.log("Error is:");
console.log(err);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ message: "Error occured" });
});
}
I resolved the issue by transferring all the stored data from CookieService, which is the main culprit of the error, to a localStorage. Just instead of storing payload and that cookie in CookieService, just transferred it to localStorage, and I didn't have any more problems. Seems like, the simpler - the better.
I have following function which calls the refresh service to get new token for authorization:
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
if(!this.isRefreshingToken) {
this.isRefreshingToken = true;
return this.authService.refreshToken()
.subscribe((response)=> {
if(response) {
const httpsReq = request.clone({
url: request.url.replace(null, this.generalService.getUserId())
});
return next.handle(this.addTokenToRequest(httpsReq, response.accessToken));
}
return <any>this.authService.logout();
}, err => {
return <any>this.authService.logout();
}, () => {
this.isRefreshingToken = false;
})
} else {
this.isRefreshingToken = false;
return this.authService.currentRefreshToken
.filter(token => token != null)
.take(1)
.map(token => {
return next.handle(this.addTokenToRequest(request, token));
})
}
}
When the response is not undefined and request is returned back it does not call the new request
Ok the thing was that the bearer was quoted like below:
But I have still one issue the request does not invoke the new request, when I refresh the page it gives data with new token, instead like I previously had unauthorized error.
We have this error handler in Express:
app.use(function (err, req, res, next) {
res.status(err.status || 500);
const stck = String(err.stack || err).split('\n').filter(function (s) {
return !String(s).match(/\/node_modules\// && String(s).match(/\//));
});
const joined = stck.join('\n');
console.error(joined);
const isProd = process.env.NODE_ENV === 'production';
const message = res.locals.message = (err.message || err);
const shortStackTrace = res.locals.shortStackTrace = isProd ? '' : joined;
const fullStackTrace = res.locals.fullStackTrace = isProd ? '': (err.stack || err);
if (req.headers['Content-Type'] === 'application/json') {
res.json({
message: message,
shortStackTrace: shortStackTrace,
fullStackTrace: fullStackTrace
});
}
else {
//locals for template have already been set
res.render('error');
}
});
My question is - we want to send back either JSON or HTML depending on the type of request. I assume looking at Content-Type header is the best way to do this. Is there any other way I should be checking?
Isn't the Content-Type header sometimes called 'content-type' (lowercase)?
I prefer using the following (this is for a 404 error but that's not important):
if (req.accepts('html')) {
// Respond with html page.
fs.readFile('404.html', 'utf-8', function(err, page) {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.write(page);
res.end();
});
} else {
if (req.accepts('json')) {
// Respond with json.
res.status(404).send({ error: 'Not found' });
} else {
// Default to plain-text. send()
res.status(404).type('txt').send('Not found');
}
}
Basically you can use the req object's accepts method to find out what you should send as a response.
When getting a json from a URL I only want to work with it, when the data is valid.
my approach so far by using JSON:
$http.get(
'data/mydata.json'
+ "?rand=" + Math.random() * 10000,
{cache: false}
)
.then(function (result) {
try {
var jsonObject = JSON.parse(JSON.stringify(result.data)); // verify that json is valid
console.log(jsonObject)
}
catch (e) {
console.log(e) // gets called when parse didn't work
}
})
However before I can do the parsing, angular already fails itself
SyntaxError: Unexpected token {
at Object.parse (native)
at fromJson (http://code.angularjs.org/1.2.0-rc.2/angular.js:908:14)
at $HttpProvider.defaults.defaults.transformResponse (http://code.angularjs.org/1.2.0-rc.2/angular.js:5735:18)
at http://code.angularjs.org/1.2.0-rc.2/angular.js:5710:12
at Array.forEach (native)
at forEach (http://code.angularjs.org/1.2.0-rc.2/angular.js:224:11)
at transformData (http://code.angularjs.org/1.2.0-rc.2/angular.js:5709:3)
at transformResponse (http://code.angularjs.org/1.2.0-rc.2/angular.js:6328:17)
at wrappedCallback (http://code.angularjs.org/1.2.0-rc.2/angular.js:9106:81)
at http://code.angularjs.org/1.2.0-rc.2/angular.js:9192:26 angular.js:7861
How can I prevent angular from throwing this error or how else should I handle verifying the JSON ?
UPDATE: Solution:
$http.get(
// url:
'data/mydata.json'
+ "?rand=" + Math.random() * 10000
,
// config:
{
cache: false,
transformResponse: function (data, headersGetter) {
try {
var jsonObject = JSON.parse(data); // verify that json is valid
return jsonObject;
}
catch (e) {
console.log("did not receive a valid Json: " + e)
}
return {};
}
}
)
You can override transformResponse in $http. Check this other answer.
I was looking for the same thing, and transformResponse does the job, BUT, I dont like using transformResponse everytime i use $http.get() or even overriding it because some $http.get() will be json and some not.
So, here is my solution:
myApp.factory('httpHandler', function($http, $q) {
function createValidJsonRequest(httpRequest) {
return {
errorMessage: function (errorMessage) {
var deferred = $q.defer();
httpRequest
.success(function (response) {
if (response != undefined && typeof response == "object"){
deferred.resolve(response);
} else {
alert(errorMessage + ": Result is not JSON type");
}
})
.error(function(data) {
deferred.reject(data);
alert(errorMessage + ": Server Error");
});
return deferred.promise;
}
};
}
return {
getJSON: function() {
return createValidJsonRequest($http.get.apply(null, arguments));
},
postJSON: function() {
return createValidJsonRequest($http.post.apply(null, arguments));
}
}
});
myApp.controller('MainCtrl', function($scope, httpHandler) {
// Option 1
httpHandler.getJSON(URL_USERS)
.errorMessage("MainCtrl -> Users")
.then(function(response) {
$scope.users = response.users;
});
// Option 2 with catch
httpHandler.getJSON(URL_NEWS)
.errorMessage("MainCtrl -> News")
.then(function(response) {
$scope.news = response.news;
})
.catch(function(result){
// do something in case of error
});
// Option 3 with POST and data
httpHandler.postJSON(URL_SAVE_NEWS, { ... })
.errorMessage("MainCtrl -> addNews")
.then(function(response) {
$scope.news.push(response.new);
});
});