error handling robustness, logger corrections, better playlist-editable checking

This commit is contained in:
2025-03-14 20:44:37 -07:00
parent ef3a055c06
commit 14eeb57a0e
4 changed files with 76 additions and 95 deletions

View File

@@ -98,7 +98,8 @@ const singleRequest = async <RespDataType>({
message = message.concat( message = message.concat(
`${error.response.status} - ${error.response.data?.error?.message}` `${error.response.status} - ${error.response.data?.error?.message}`
); );
res?.status(error.response.status).send(error.response.data); if (res && !res.headersSent)
res.status(error.response.status).send(error.response.data);
logger.warn(message, { logger.warn(message, {
response: { response: {
data: error.response.data, data: error.response.data,
@@ -109,13 +110,14 @@ const singleRequest = async <RespDataType>({
} else if (error.request) { } else if (error.request) {
// Request sent, but no response received // Request sent, but no response received
message = message.concat("No response"); message = message.concat("No response");
res?.status(504).send({ message }); if (res && !res.headersSent) res.status(504).send({ message });
logger.error(message, { error }); logger.error(message, { error });
return { error, message }; return { error, message };
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
message = message.concat("Request failed"); message = message.concat("Request failed");
res?.status(500).send({ message: "Internal Server Error" }); if (res && !res.headersSent)
res.status(500).send({ message: "Internal Server Error" });
logger.error(message, { error }); logger.error(message, { error });
return { error, message }; return { error, message };
} }
@@ -168,9 +170,7 @@ const getCurrentUsersPlaylistsNextPage: (
}); });
}; };
interface GetPlaylistDetailsFirstPageArgs interface GetPlaylistDetailsFirstPageArgs extends EndpointHandlerWithResArgs {
extends Omit<EndpointHandlerWithResArgs, "res"> {
res?: Res;
initialFields: string; initialFields: string;
playlistID: string; playlistID: string;
} }
@@ -257,9 +257,7 @@ const removePlaylistItems: (
// non-endpoints, i.e. convenience wrappers // non-endpoints, i.e. convenience wrappers
// --------- // ---------
interface CheckPlaylistEditableArgs interface CheckPlaylistEditableArgs extends EndpointHandlerWithResArgs {
extends Omit<EndpointHandlerWithResArgs, "res"> {
res?: Res;
playlistID: string; playlistID: string;
userID: string; userID: string;
} }
@@ -276,14 +274,13 @@ const checkPlaylistEditable: (
playlistID, playlistID,
userID, userID,
}) => { }) => {
let checkFields = ["collaborative", "owner(id)"]; let checkFields = ["collaborative", "owner(id)", "name"];
let args: GetPlaylistDetailsFirstPageArgs = { const { resp, error, message } = await getPlaylistDetailsFirstPage({
res,
authHeaders, authHeaders,
initialFields: checkFields.join(), initialFields: checkFields.join(),
playlistID, playlistID,
}; });
if (res) args.res = res;
const { resp, error, message } = await getPlaylistDetailsFirstPage(args);
if (!resp) return { status: false, error, message }; if (!resp) return { status: false, error, message };
// https://web.archive.org/web/20241226081630/https://developer.spotify.com/documentation/web-api/concepts/playlists#:~:text=A%20playlist%20can%20also%20be%20made%20collaborative // https://web.archive.org/web/20241226081630/https://developer.spotify.com/documentation/web-api/concepts/playlists#:~:text=A%20playlist%20can%20also%20be%20made%20collaborative

View File

@@ -79,10 +79,10 @@ const callback: RequestHandler = async (req, res) => {
Authorization: `Bearer ${req.session.accessToken}`, Authorization: `Bearer ${req.session.accessToken}`,
}; };
} else { } else {
logger.error("login failed", { statusCode: tokenResponse.status });
res res
.status(tokenResponse.status) .status(tokenResponse.status)
.send({ message: "Error: Login failed" }); .send({ message: "Error: Login failed" });
logger.error("login failed", { statusCode: tokenResponse.status });
return null; return null;
} }

View File

@@ -154,9 +154,7 @@ const updateUser: RequestHandler = async (req, res) => {
}); });
if (delNum !== deleted.length) { if (delNum !== deleted.length) {
res.status(500).send({ message: "Internal Server Error" }); res.status(500).send({ message: "Internal Server Error" });
logger.error("Could not remove all old playlists", { logger.error("Could not remove all old playlists");
error: new Error("Playlists.destroy failed?"),
});
return null; return null;
} }
} }
@@ -170,9 +168,7 @@ const updateUser: RequestHandler = async (req, res) => {
); );
if (addPls.length !== added.length) { if (addPls.length !== added.length) {
res.status(500).send({ message: "Internal Server Error" }); res.status(500).send({ message: "Internal Server Error" });
logger.error("Could not add all new playlists", { logger.error("Could not add all new playlists");
error: new Error("Playlists.bulkCreate failed?"),
});
return null; return null;
} }
} }
@@ -189,9 +185,7 @@ const updateUser: RequestHandler = async (req, res) => {
}); });
} catch (error) { } catch (error) {
res.status(500).send({ message: "Internal Server Error" }); res.status(500).send({ message: "Internal Server Error" });
logger.error("Could not update playlist names", { logger.error("Could not update playlist names");
error: new Error("Playlists.update failed?"),
});
return null; return null;
} }
@@ -272,13 +266,13 @@ const createLink: RequestHandler = async (req, res) => {
fromPl = parseSpotifyLink(req.body.from); fromPl = parseSpotifyLink(req.body.from);
toPl = parseSpotifyLink(req.body.to); toPl = parseSpotifyLink(req.body.to);
if (fromPl.type !== "playlist" || toPl.type !== "playlist") { if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
res.status(400).send({ message: "Link is not a playlist" }); res.status(400).send({ message: "Links must be playlist links!" });
logger.info("non-playlist link provided", { from: fromPl, to: toPl }); logger.debug("non-playlist link provided");
return null; return null;
} }
} catch (error) { } catch (error) {
res.status(400).send({ message: "Could not parse link" }); res.status(400).send({ message: "Could not parse link" });
logger.warn("parseSpotifyLink", { error }); logger.info("parseSpotifyLink", { error });
return null; return null;
} }
@@ -291,8 +285,8 @@ const createLink: RequestHandler = async (req, res) => {
// if playlists are unknown // if playlists are unknown
if (![fromPl, toPl].every((pl) => playlistIDs.includes(pl.id))) { if (![fromPl, toPl].every((pl) => playlistIDs.includes(pl.id))) {
res.status(404).send({ message: "Playlists out of sync." }); res.status(404).send({ message: "Unknown playlists, resync first." });
logger.warn("unknown playlists, resync"); logger.debug("unknown playlists, resync");
return null; return null;
} }
@@ -304,7 +298,7 @@ const createLink: RequestHandler = async (req, res) => {
}); });
if (existingLink) { if (existingLink) {
res.status(409).send({ message: "Link already exists!" }); res.status(409).send({ message: "Link already exists!" });
logger.info("link already exists"); logger.debug("link already exists");
return null; return null;
} }
@@ -322,8 +316,8 @@ const createLink: RequestHandler = async (req, res) => {
if (newGraph.detectCycle()) { if (newGraph.detectCycle()) {
res res
.status(400) .status(400)
.send({ message: "Proposed link cannot cause a cycle in the graph" }); .send({ message: "The link cannot cause a cycle in the graph." });
logger.warn("potential cycle detected"); logger.debug("potential cycle detected");
return null; return null;
} }
@@ -334,9 +328,7 @@ const createLink: RequestHandler = async (req, res) => {
}); });
if (!newLink) { if (!newLink) {
res.status(500).send({ message: "Internal Server Error" }); res.status(500).send({ message: "Internal Server Error" });
logger.error("Could not create link", { logger.error("Could not create link");
error: new Error("Links.create failed?"),
});
return null; return null;
} }
@@ -364,13 +356,13 @@ const removeLink: RequestHandler = async (req, res) => {
fromPl = parseSpotifyLink(req.body.from); fromPl = parseSpotifyLink(req.body.from);
toPl = parseSpotifyLink(req.body.to); toPl = parseSpotifyLink(req.body.to);
if (fromPl.type !== "playlist" || toPl.type !== "playlist") { if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
res.status(400).send({ message: "Link is not a playlist" }); res.status(400).send({ message: "Links must be playlist links!" });
logger.info("non-playlist link provided", { from: fromPl, to: toPl }); logger.debug("non-playlist link provided");
return null; return null;
} }
} catch (error) { } catch (error) {
res.status(400).send({ message: "Could not parse link" }); res.status(400).send({ message: "Could not parse link" });
logger.warn("parseSpotifyLink", { error }); logger.info("parseSpotifyLink", { error });
return null; return null;
} }
@@ -382,7 +374,7 @@ const removeLink: RequestHandler = async (req, res) => {
}); });
if (!existingLink) { if (!existingLink) {
res.status(409).send({ message: "Link does not exist!" }); res.status(409).send({ message: "Link does not exist!" });
logger.warn("link does not exist"); logger.debug("link does not exist");
return null; return null;
} }
@@ -393,9 +385,7 @@ const removeLink: RequestHandler = async (req, res) => {
}); });
if (!removedLink) { if (!removedLink) {
res.status(500).send({ message: "Internal Server Error" }); res.status(500).send({ message: "Internal Server Error" });
logger.error("Could not remove link", { logger.error("Could not remove link");
error: new Error("Links.destroy failed?"),
});
return null; return null;
} }
@@ -560,12 +550,12 @@ const populateSingleLink: RequestHandler = async (req, res) => {
toPl = parseSpotifyLink(link.to); toPl = parseSpotifyLink(link.to);
if (fromPl.type !== "playlist" || toPl.type !== "playlist") { if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
res.status(400).send({ message: "Link is not a playlist" }); res.status(400).send({ message: "Link is not a playlist" });
logger.info("non-playlist link provided", link); logger.debug("non-playlist link provided", { link });
return null; return null;
} }
} catch (error) { } catch (error) {
res.status(400).send({ message: "Could not parse link" }); res.status(400).send({ message: "Could not parse link" });
logger.warn("parseSpotifyLink", { error }); logger.info("parseSpotifyLink", { error });
return null; return null;
} }
@@ -577,21 +567,21 @@ const populateSingleLink: RequestHandler = async (req, res) => {
}); });
if (!existingLink) { if (!existingLink) {
res.status(409).send({ message: "Link does not exist!" }); res.status(409).send({ message: "Link does not exist!" });
logger.warn("link does not exist", { link }); logger.debug("link does not exist", { link });
return null; return null;
} }
if ( const editableResp = await checkPlaylistEditable({
!(
await checkPlaylistEditable({
authHeaders,
res, res,
authHeaders,
playlistID: fromPl.id, playlistID: fromPl.id,
userID: uID, userID: uID,
}) });
).status if (!editableResp.status) {
) res.status(403).send({ message: editableResp.message });
logger.debug(editableResp.message, { editableResp });
return null; return null;
}
const fromTracks = await _getPlaylistTracks({ const fromTracks = await _getPlaylistTracks({
res, res,
@@ -659,12 +649,12 @@ const populateChain: RequestHandler = async (req, res) => {
rootPl = parseSpotifyLink(root); rootPl = parseSpotifyLink(root);
if (rootPl.type !== "playlist") { if (rootPl.type !== "playlist") {
res.status(400).send({ message: "Link is not a playlist" }); res.status(400).send({ message: "Link is not a playlist" });
logger.info("non-playlist link provided", root); logger.debug("non-playlist link provided");
return null; return null;
} }
} catch (error) { } catch (error) {
res.status(400).send({ message: "Could not parse link" }); res.status(400).send({ message: "Could not parse link" });
logger.warn("parseSpotifyLink", { error }); logger.info("parseSpotifyLink", { error });
return null; return null;
} }
@@ -691,14 +681,26 @@ const populateChain: RequestHandler = async (req, res) => {
const editableStatuses = await Promise.all( const editableStatuses = await Promise.all(
affectedPlaylists.map((pl) => { affectedPlaylists.map((pl) => {
return checkPlaylistEditable({ return checkPlaylistEditable({
res,
authHeaders, authHeaders,
playlistID: pl, playlistID: pl,
userID: uID, userID: uID,
}); });
}) })
); );
if (editableStatuses.some((statusObj) => statusObj.status === false)) if (res.headersSent) return null; // error, resp sent and logged in singleRequest
// else, respond with the non-editable playlists
const nonEditablePlaylists = editableStatuses.filter(
(statusObj) => statusObj.status === false
);
if (nonEditablePlaylists.length > 0) {
let message =
"Cannot edit one or more playlists: " +
nonEditablePlaylists.map((pl) => pl.error?.playlistName).join(", ");
res.status(403).send({ message });
logger.debug(message, { nonEditablePlaylists });
return null; return null;
}
const affectedPlaylistsTracks = await Promise.all( const affectedPlaylistsTracks = await Promise.all(
affectedPlaylists.map((pl) => { affectedPlaylists.map((pl) => {
@@ -830,12 +832,12 @@ const pruneSingleLink: RequestHandler = async (req, res) => {
toPl = parseSpotifyLink(link.to); toPl = parseSpotifyLink(link.to);
if (fromPl.type !== "playlist" || toPl.type !== "playlist") { if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
res.status(400).send({ message: "Link is not a playlist" }); res.status(400).send({ message: "Link is not a playlist" });
logger.info("non-playlist link provided", link); logger.debug("non-playlist link provided");
return null; return null;
} }
} catch (error: any) { } catch (error: any) {
res.status(400).send({ message: error.message }); res.status(400).send({ message: error.message });
logger.warn("parseSpotifyLink", { error }); logger.info("parseSpotifyLink", { error });
return null; return null;
} }
@@ -851,17 +853,17 @@ const pruneSingleLink: RequestHandler = async (req, res) => {
return null; return null;
} }
if ( const editableResp = await checkPlaylistEditable({
!(
await checkPlaylistEditable({
authHeaders,
res, res,
authHeaders,
playlistID: toPl.id, playlistID: toPl.id,
userID: uID, userID: uID,
}) });
).status if (!editableResp.status) {
) res.status(403).send({ message: editableResp.message });
logger.debug(editableResp.message, { editableResp });
return null; return null;
}
const fromTracks = await _getPlaylistTracks({ const fromTracks = await _getPlaylistTracks({
res, res,

View File

@@ -2,36 +2,22 @@ import path from "path";
import { createLogger, transports, config, format, type Logger } from "winston"; import { createLogger, transports, config, format, type Logger } from "winston";
const { combine, timestamp, printf, errors } = format; const { combine, timestamp, printf } = format;
const metaFormat = (meta: object) => { const metaFormat = (meta: object) => {
const disallowedKeySets = [{ type: Error, keys: ["stack"] }];
if (Object.keys(meta).length > 0) if (Object.keys(meta).length > 0)
return ( return "\n" + JSON.stringify(meta, null, "\t");
"\n" +
JSON.stringify(
meta,
Object.getOwnPropertyNames(meta).filter((key) => {
for (const pair of disallowedKeySets) {
if (meta instanceof pair.type) {
return !pair.keys.includes(key);
}
}
return true;
}),
"\t"
)
);
return ""; return "";
}; };
const logFormat = printf(({ level, message, label, timestamp, ...meta }) => { const logFormat = printf(({ level, message, timestamp, stack, ...meta }) => {
const errorObj: Error = meta["error"] as Error; const errorObj: Error = meta["error"] as Error;
if (errorObj) { if (errorObj) {
const stackStr = errorObj["stack"];
return ( return (
`${timestamp} [${level.toUpperCase()}]: ${message}` + // line 1 `${timestamp} [${level.toUpperCase()}]: ${message}` + // line 1
`${metaFormat(errorObj)}\n` + // metadata `${metaFormat(errorObj)}\n` + // metadata
`${errorObj["stack"] ?? ""}` // stack trace if any `${stackStr}` // stack trace if any
); );
} }
return `${timestamp} [${level.toUpperCase()}]: ${message}${metaFormat(meta)}`; return `${timestamp} [${level.toUpperCase()}]: ${message}${metaFormat(meta)}`;
@@ -39,11 +25,7 @@ const logFormat = printf(({ level, message, label, timestamp, ...meta }) => {
const winstonLogger: Logger = createLogger({ const winstonLogger: Logger = createLogger({
levels: config.npm.levels, levels: config.npm.levels,
format: combine( format: combine(timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), logFormat),
errors({ stack: true }),
timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
logFormat
),
transports: [ transports: [
new transports.Console({ level: "info" }), new transports.Console({ level: "info" }),
new transports.File({ new transports.File({
@@ -58,7 +40,7 @@ const winstonLogger: Logger = createLogger({
], ],
}); });
winstonLogger.on("error", (error) => winstonLogger.on("error", (error) =>
winstonLogger.error("Error inside logger", { error }) console.error("Error inside logger", error)
); );
winstonLogger.exceptions.handle( winstonLogger.exceptions.handle(
new transports.File({ new transports.File({