Skip to content

Commit a8effb8

Browse files
justandrasnytaminJulusian
committed
fix: UserError getting lost when returned from jobWorker
Co-authored-by: Johan Nyman <[email protected]> Co-authored-by: Julian Waller <[email protected]>
1 parent 9cf4b58 commit a8effb8

File tree

16 files changed

+115
-106
lines changed

16 files changed

+115
-106
lines changed

meteor/__mocks__/_extendJest.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ expect.extend({
4242
received = received.error
4343
}
4444

45-
if (UserError.isUserError(received)) {
45+
if (received instanceof UserError) {
4646
const expected = UserError.create(msg, args)
4747
const received2 = clone(received)
4848

@@ -68,10 +68,10 @@ expect.extend({
6868
received = received.error
6969
}
7070

71-
if (UserError.isUserError(received)) {
72-
const pass = !!received.rawError.toString().match(regexp)
71+
if (received instanceof UserError) {
72+
const pass = !!received.toString().match(regexp)
7373
return {
74-
message: () => `expected ${received.rawError} to match ${regexp}`,
74+
message: () => `expected ${stringifyError(received)} to match ${regexp}`,
7575
pass: pass,
7676
}
7777
} else {

meteor/server/api/client.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,10 @@ import { assertConnectionHasOneOfPermissions } from '../security/auth'
3434

3535
function rewrapError(methodName: string, e: any): ClientAPI.ClientResponseError {
3636
const userError = UserError.fromUnknown(e)
37-
3837
logger.info(`UserAction "${methodName}" failed: ${userError.toErrorString()}`)
3938

4039
// Forward the error to the caller
41-
return ClientAPI.responseError(userError, userError.errorCode)
40+
return ClientAPI.responseError(userError)
4241
}
4342

4443
export namespace ServerClientAPI {
@@ -199,7 +198,6 @@ export namespace ServerClientAPI {
199198
// Just run and return right away:
200199
try {
201200
const result = await fcn({})
202-
203201
return ClientAPI.responseSuccess(result)
204202
} catch (e) {
205203
return rewrapError(methodName, e)

meteor/server/api/rest/v1/index.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ koaRouter.use(bodyParser())
5050

5151
function extractErrorCode(e: unknown): number {
5252
if (ClientAPI.isClientResponseError(e)) {
53-
return e.errorCode
54-
} else if (UserError.isUserError(e)) {
53+
return e.error.errorCode
54+
} else if (UserError.isStringifiedUserErrorObject(e) || e instanceof UserError) {
5555
return e.errorCode
5656
} else if ((e as Meteor.Error).error && typeof (e as Meteor.Error).error === 'number') {
5757
return (e as Meteor.Error).error as number
@@ -61,15 +61,17 @@ function extractErrorCode(e: unknown): number {
6161
}
6262

6363
function validateUserError(e: unknown): UserError | undefined {
64-
if (UserError.isUserError(e)) {
65-
return e as UserError
64+
if (e instanceof UserError) {
65+
return e
66+
} else if (UserError.isStringifiedUserErrorObject(e)) {
67+
return UserError.fromUnknown(e)
6668
}
6769
}
6870

6971
function extractErrorUserMessage(e: unknown): string {
7072
if (ClientAPI.isClientResponseError(e)) {
7173
return translateMessage(e.error.userMessage, interpollateTranslation)
72-
} else if (UserError.isUserError(e)) {
74+
} else if (UserError.isStringifiedUserErrorObject(e) || e instanceof UserError) {
7375
return translateMessage(e.userMessage, interpollateTranslation)
7476
} else if ((e as Meteor.Error).reason && typeof (e as Meteor.Error).reason === 'string') {
7577
return (e as Meteor.Error).reason as string
@@ -147,27 +149,27 @@ function sofieAPIRequest<API, Params, Body, Response>(
147149
ctx.request.body as unknown as Body
148150
)
149151
if (ClientAPI.isClientResponseError(response)) {
150-
// We wrap our error in another error so the status code override is not lost
151-
throw UserError.from(response.error.rawError, response.error.key, undefined, response.errorCode)
152+
throw UserError.fromSerialized(response.error)
152153
}
153154
ctx.body = JSON.stringify({ status: response.success, result: response.result })
154155
ctx.status = response.success
155156
} catch (e) {
156157
console.log('LOOK HERE', e)
158+
157159
const userError = validateUserError(e)
158160
const errCode = extractErrorCode(userError)
159161
let errMsg = extractErrorUserMessage(userError)
160162
// Get the fallback messages of the endpoint
161163
const fallbackMsgs = errMsgFallbacks.get(errCode)
162164

163-
if (userError?.rawError.message) {
165+
if (userError?.message) {
164166
// If we have a detailed arbitrary error message then return that together with the standard error message.
165167
errMsg = `${translateMessage(
166168
{
167169
key: errMsg,
168170
},
169171
interpollateTranslation
170-
)} - ${userError?.rawError.message}`
172+
)} - ${userError?.message}`
171173
} else if (fallbackMsgs) {
172174
// If no detailed error message is provided then return the fallback error messages.
173175
const msgConcat = {

meteor/server/worker/worker.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { MongoQuery } from '@sofie-automation/corelib/dist/mongo'
2222
import { UserActionsLog } from '../collections'
2323
import { MetricsCounter } from '@sofie-automation/corelib/dist/prometheus'
2424
import { isInTestWrite } from '../security/securityVerify'
25+
import { UserError } from '@sofie-automation/corelib/dist/error'
2526

2627
const FREEZE_LIMIT = 1000 // how long to wait for a response to a Ping
2728
const RESTART_TIMEOUT = 30000 // how long to wait for a restart to complete before throwing an error
@@ -107,7 +108,8 @@ async function jobFinished(
107108
}
108109

109110
if (job.completionHandler) {
110-
job.completionHandler(startedTime, finishedTime, err, result)
111+
const userError = err ? UserError.tryFromJSON(err) || new Error(err) : undefined
112+
job.completionHandler(startedTime, finishedTime, userError, result)
111113
}
112114
}
113115
}

packages/corelib/src/error.ts

Lines changed: 55 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -126,29 +126,23 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = {
126126
[UserErrorMessage.RateLimitExceeded]: t(`Rate limit exceeded`),
127127
}
128128

129-
export interface UserErrorObj {
129+
export interface SerializedUserError {
130130
readonly errorCode: number
131131

132132
/** The raw Error that was thrown */
133-
readonly rawError: Error
133+
rawError: Pick<Error, 'name' | 'message' | 'stack'>
134134
/** The UserErrorMessage key (for matching certain error) */
135135
readonly key: UserErrorMessage
136136
/** The translatable string for the key */
137137
readonly userMessage: ITranslatableMessage
138138
}
139139

140-
export interface StringifiedUserErrorObject extends Omit<UserErrorObj, 'rawError'> {
141-
rawError: Pick<Error, 'name' | 'message' | 'stack'>
142-
}
143-
144-
export type StringifiedErrorObject = Omit<Error, 'cause'>
145-
146-
export class UserError extends Error implements UserErrorObj {
140+
export class UserError extends Error {
147141
public readonly errorCode: number
148142

149143
private constructor(
150144
/** The raw Error that was thrown */
151-
public readonly rawError: Error,
145+
rawError: SerializedUserError['rawError'],
152146
/** The UserErrorMessage key (for matching certain error) */
153147
public readonly key: UserErrorMessage,
154148
/** The translatable string for the key */
@@ -157,21 +151,31 @@ export class UserError extends Error implements UserErrorObj {
157151
errorCode: number | undefined
158152
) {
159153
super()
160-
this.errorCode = errorCode ?? 500
161-
}
162154

163-
public toString(): string {
164-
return UserError.toJSON(this)
155+
// Populate the error properties:
156+
this.message = rawError.message
157+
this.stack = rawError.stack
158+
this.name = 'UserError'
159+
160+
this.errorCode = errorCode ?? 500
165161
}
166162

167163
/** Create a UserError with a custom error for the log */
168164
static from(err: Error, key: UserErrorMessage, args?: { [k: string]: any }, errCode?: number): UserError {
169165
return new UserError(err, key, { key: UserErrorMessagesTranslations[key], args }, errCode)
170166
}
167+
/** Create a UserError duplicating the same error for the log */
168+
static create(key: UserErrorMessage, args?: { [k: string]: any }, errorCode?: number): UserError {
169+
return UserError.from(new Error(UserErrorMessagesTranslations[key]), key, args, errorCode)
170+
}
171+
static fromSerialized(o: SerializedUserError): UserError {
172+
return new UserError(o.rawError, o.key, o.userMessage, o.errorCode)
173+
}
171174
/** Create a UserError from an unknown possibly error input */
172175
static fromUnknown(err: unknown, errorCode?: number): UserError {
173176
if (err instanceof UserError) return err
174-
if (this.isUserError(err)) {
177+
178+
if (this.isStringifiedUserErrorObject(err)) {
175179
return new UserError(err.rawError, err.key, err.userMessage, err.errorCode)
176180
}
177181
const err2 = err instanceof Error ? err : new Error(stringifyError(err))
@@ -183,69 +187,61 @@ export class UserError extends Error implements UserErrorObj {
183187
)
184188
}
185189

186-
/** Create a UserError duplicating the same error for the log */
187-
static create(key: UserErrorMessage, args?: { [k: string]: any }, errorCode?: number): UserError {
188-
return UserError.from(new Error(UserErrorMessagesTranslations[key]), key, args, errorCode)
189-
}
190-
191190
static tryFromJSON(str: string): UserError | undefined {
191+
let p: SerializedUserError | undefined
192192
try {
193-
const p = UserError.fromJSON(str)
194-
if (p) {
195-
const newError = p.rawError as Error
196-
const newUserError = new UserError(newError, p.key, p.userMessage, p.errorCode)
197-
return newUserError
198-
} else {
199-
return undefined
200-
}
193+
p = UserError.fromJSON(str)
194+
if (!p) return undefined
201195
} catch (_e: any) {
196+
// Ignore JSON parsing error
197+
return undefined
198+
}
199+
200+
if (this.isStringifiedUserErrorObject(p)) {
201+
return new UserError(p.rawError, p.key, p.userMessage, p.errorCode)
202+
} else {
202203
return undefined
203204
}
204205
}
205206

206-
static toJSON(e: UserErrorObj): string {
207-
const o: StringifiedUserErrorObject = {
207+
static serialize(e: UserError): SerializedUserError {
208+
const o: SerializedUserError = {
208209
rawError: {
209-
name: e.rawError.name,
210-
message: e.rawError.message,
211-
stack: e.rawError.stack,
210+
name: e.name,
211+
message: e.message,
212+
stack: e.stack,
212213
},
213214
userMessage: e.userMessage,
214215
key: e.key,
215216
errorCode: e.errorCode,
216217
}
217-
return JSON.stringify(o)
218+
return o
218219
}
219-
static fromJSON(str: string): StringifiedUserErrorObject | undefined {
220+
static toJSON(e: UserError): string {
221+
return JSON.stringify(this.serialize(e))
222+
}
223+
static fromJSON(str: string): SerializedUserError | undefined {
220224
const o = JSON.parse(str)
221-
if (isStringifiedUserErrorObject(o)) return o
225+
if (this.isStringifiedUserErrorObject(o)) return o
222226
return undefined
223227
}
224-
225-
static isUserError(e: unknown): e is UserErrorObj {
226-
return !!e && typeof e === 'object' && 'rawError' in e && 'userMessage' in e && 'key' in e
228+
static isStringifiedUserErrorObject(o: unknown): o is SerializedUserError {
229+
if (!o || typeof o !== 'object') return false
230+
const errorObject = o as SerializedUserError
231+
232+
return (
233+
'rawError' in errorObject &&
234+
!!errorObject.rawError &&
235+
typeof errorObject.rawError === 'object' &&
236+
errorObject.rawError &&
237+
typeof errorObject.rawError.message === 'string' &&
238+
isTranslatableMessage(errorObject.userMessage) &&
239+
typeof errorObject.errorCode === 'number' &&
240+
typeof errorObject.key === 'number'
241+
)
227242
}
228243

229244
toErrorString(): string {
230-
return `${translateMessage(this.userMessage, interpollateTranslation)}\n${stringifyError(this.rawError)}`
245+
return `${translateMessage(this.userMessage, interpollateTranslation)}\n${stringifyError(this)}`
231246
}
232247
}
233-
234-
function isStringifiedUserErrorObject(o: any): o is StringifiedUserErrorObject {
235-
return (
236-
o &&
237-
typeof o === 'object' &&
238-
'rawError' in o &&
239-
o.rawError &&
240-
isStringifiedErrorObject(o.rawError) &&
241-
isTranslatableMessage(o.userMessage) &&
242-
typeof o.errorCode === 'number' &&
243-
typeof o.key === 'number' &&
244-
o.key >= 0 &&
245-
o.key < Object.keys(UserErrorMessage).length / 2
246-
)
247-
}
248-
249-
function isStringifiedErrorObject(o: any): o is StringifiedErrorObject {
250-
return o && typeof o === 'object' && 'name' in o && 'message' in o && 'stack' in o
251-
}

packages/job-worker/src/manager.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { WorkerId } from '@sofie-automation/corelib/dist/dataModel/Ids'
2-
import { UserError } from '@sofie-automation/corelib/dist/error'
32
import { JobSpec } from './main.js'
43

54
export interface JobManager {
65
jobFinished: (
76
id: string,
87
startedTime: number,
98
finishedTime: number,
10-
error: null | Error | UserError,
9+
error: null | string, // Stringified UserError
1110
result: any
1211
) => Promise<void>
1312
// getNextJob: (queueName: string) => Promise<JobSpec>

packages/job-worker/src/playout/take.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart
7272
)
7373
}
7474
}
75-
7675
if (lastTakeTime && now - lastTakeTime < context.studio.settings.minimumTakeSpan) {
7776
logger.debug(
7877
`Time since last take is shorter than ${context.studio.settings.minimumTakeSpan} for ${

packages/job-worker/src/workers/parent-base.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Promisify, ThreadedClassManager } from 'threadedclass'
1919
import { StatusCode } from '@sofie-automation/blueprints-integration'
2020
import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections'
2121
import { WorkerThreadStatus } from '@sofie-automation/corelib/dist/dataModel/WorkerThreads'
22-
import { UserError } from '@sofie-automation/corelib/dist/error'
22+
import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error'
2323
import { sleep } from '@sofie-automation/shared-lib/dist/lib/lib'
2424

2525
export enum ThreadStatus {
@@ -306,9 +306,7 @@ export abstract class WorkerParentBase {
306306
job.id,
307307
startTime,
308308
endTime,
309-
result.error
310-
? (UserError.tryFromJSON(result.error) ?? new Error(result.error))
311-
: null,
309+
result.error,
312310
result.result
313311
)
314312

@@ -324,7 +322,13 @@ export abstract class WorkerParentBase {
324322
logger.error(`Job errored ${job.id} "${job.name}": ${stringifyError(e)}`)
325323

326324
this.#watchdogJobStarted = undefined
327-
await this.#jobManager.jobFinished(job.id, startTime, Date.now(), error, null)
325+
await this.#jobManager.jobFinished(
326+
job.id,
327+
startTime,
328+
Date.now(),
329+
UserError.toJSON(UserError.from(error, UserErrorMessage.InternalError)),
330+
null
331+
)
328332
}
329333

330334
// Ensure all locks have been freed after the job

0 commit comments

Comments
 (0)