All Articles

Invisible Bugs: File Bypass & Missing Worker Runs

· 8 min read · Humza Tareen
Bug Fixing Validation TypeScript Prisma Production

Some bugs never show up in staging. They sit behind assumptions that only break when a real reviewer picks the wrong file type, or when an operator uses the admin panel in a way your happy-path tests never exercised. This post is about two such production issues on the platform I work on: a file upload validation bypass in the review system, and a missing background “worker run” record when admins bulk-approve tasks. Both looked fine in isolation. Both failed only when the shortcut path diverged from the full workflow.

The through-line is not clever TypeScript tricks. It is postconditions: whatever the normal flow guarantees after a step—matching file types, durable pipeline markers—the bypass must guarantee the same thing, or downstream queries and jobs will lie to you quietly until someone notices missing rows in a tab.

Bug 1: When “allowed extension” is not enough

The setup

Expert reviewers on the platform upload revised deliverables for tasks. The upload endpoint applies a global allowlist of extensions—think .docx, .xlsx, .pdf, and a handful of others. That check is reasonable: it blocks executables, odd archives, and obvious mistakes before anything hits object storage.

In development, we always uploaded the same kind of file the task started with. Tests used fixtures that matched the original artifact. No one tried to “fix” a Word report by dropping in a PDF, so the allowlist looked like sufficient protection.

The bug

Consider a task whose original artifact is a model-generated report.docx. A reviewer uploads a revised version—but they choose a .pdf instead. The global allowlist happily accepts PDFs, so the request passes validation. The problem is downstream: document processing, diff tooling, and merge steps assume the revised file is the same format as the original. Swapping .docx for .pdf does not trip the allowlist; it breaks the pipeline that expects a single consistent type through the revision boundary.

From the server’s perspective, nothing “failed” at upload time. From the user’s perspective, things fail later in ways that are hard to attribute to the upload step. That is the definition of an invisible bug: locally valid input, globally invalid state.

The fix: two layers

We added two-layer validation. On the server, a function validateFileTypeMatch() compares the uploaded file’s extension to the original task artifact’s extension. If they differ, the API rejects the upload with a clear error, even when both extensions appear on the global allowlist.

On the client, each row in the file list gets a dropzone whose accept attribute is restricted to the original file’s extension. If the source was .docx, the picker only offers Word-class inputs; the user never reaches the misleading success path for a PDF in the first place.

To avoid breaking older clients, originalFileName (or equivalent metadata) is optional on the request. When it is missing, we fall back to allowlist-only validation—the legacy behavior—so nothing hard-crashes for consumers that have not been updated yet. New clients send the field; the server enforces the match when present.

function extname(filename: string): string {
  const base = filename.split(/[/\\]/).pop() ?? "";
  const i = base.lastIndexOf(".");
  return i === -1 ? "" : base.slice(i).toLowerCase();
}

export function validateFileTypeMatch(
  originalFileName: string | undefined | null,
  uploadedFileName: string
): { ok: true } | { ok: false; reason: string } {
  if (!originalFileName?.trim()) {
    return { ok: true };
  }

  const a = extname(originalFileName);
  const b = extname(uploadedFileName);

  if (a === "" || b === "") {
    return { ok: false, reason: "missing_extension" };
  }

  if (a !== b) {
    return { ok: false, reason: `type_mismatch:${a}->${b}` };
  }

  return { ok: true };
}

For the UI, tying accept to the original artifact keeps mistakes from becoming API calls:

function acceptForOriginal(originalFileName?: string | null): string | undefined {
  if (!originalFileName?.trim()) return undefined;

  const ext = extname(originalFileName);
  if (!ext) return undefined;

  if (ext === ".docx" || ext === ".doc") return ".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document";
  if (ext === ".pdf") return ".pdf,application/pdf";
  if (ext === ".xlsx" || ext === ".xls") return ".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

  return ext;
}

export function RevisedFileDropzone(props: {
  originalFileName?: string | null;
  onFile: (file: File) => void;
}) {
  const accept = acceptForOriginal(props.originalFileName);

  return (
    <input
      type="file"
      accept={accept}
      onChange={(e) => {
        const file = e.target.files?.[0];
        if (file) props.onFile(file);
      }}
    />
  );
}

Testing

We added ten focused tests: pure validation cases, route-level integration, explicit mismatch rejection, case-insensitive extension comparison, and the backward-compat path when originalFileName is absent. Isolating the matcher in a small module made it cheap to enumerate edge cases without standing up the whole storage stack.

Bug 2: Admin bulk-approve and the tab that stayed empty

The setup

The admin panel exposes bulk status changes: select many tasks and move them to an approved outcome—labels like passed, approved, or auto-accepted depending on your domain vocabulary. A separate view, call it “Delivery Completed,” lists work that has fully cleared internal quality gates.

That tab is driven by a database query with two predicates: the task stage must be COMPLETED, and there must exist a row for a specific worker run type—say an expert quality check—marking that step as finished. The normal reviewer flow always creates that worker run when someone submits an assessment, so in manual testing the tab populated predictably.

The bug

Bulk admin approval does not go through the reviewer submission path. It updates status (and related fields) directly. The query for “Delivery Completed” still required the expert quality check run. Tasks that admins approved never got that run inserted, so they never appeared in the tab—even though, from the admin’s point of view, the tasks were “done.”

This is another invisible failure: no thrown exception, no red banner. Just an empty slice of the product for a class of tasks that took a legitimate shortcut.

The fix: align the shortcut with the full path

When bulk operations move tasks into an approved terminal family (PASSED, APPROVED, AUTO_ACCEPTED), the server now upserts the expert quality check worker run with status COMPLETED. The upsert uses an empty update clause (or equivalent no-op on conflict) so that if the normal flow already wrote a run, we do not clobber timestamps or metadata.

Worker-run creation is intentionally non-blocking: if persisting the run fails, the status transition still commits, and the API returns success with a partial warning so operators know something needs follow-up instead of silently losing the bulk action.

type WorkerRunType = "EXPERT_QUALITY_CHECK";
type WorkerRunStatus = "COMPLETED" | "FAILED" | "PENDING";

async function ensureExpertQualityCheckRun(
  tx: Prisma.TransactionClient,
  taskId: string
) {
  await tx.workerRun.upsert({
    where: {
      taskId_type: { taskId, type: "EXPERT_QUALITY_CHECK" },
    },
    create: {
      taskId,
      type: "EXPERT_QUALITY_CHECK",
      status: "COMPLETED",
      completedAt: new Date(),
    },
    update: {},
  });
}
type BulkApplyResult = {
  taskId: string;
  ok: true;
  warnings?: string[];
} | {
  taskId: string;
  ok: false;
  error: string;
};

async function applyApprovedTransition(
  prisma: PrismaClient,
  taskId: string,
  nextStatus: "PASSED" | "APPROVED" | "AUTO_ACCEPTED"
): Promise<BulkApplyResult> {
  const warnings: string[] = [];

  await prisma.$transaction(async (tx) => {
    await tx.task.update({
      where: { id: taskId },
      data: { status: nextStatus, stage: "COMPLETED", updatedAt: new Date() },
    });
  });

  try {
    await prisma.$transaction(async (tx) => {
      await ensureExpertQualityCheckRun(tx, taskId);
    });
  } catch {
    warnings.push("worker_run_upsert_failed");
  }

  return warnings.length
    ? { taskId, ok: true, warnings }
    : { taskId, ok: true };
}

We do not create this run for negative or in-review outcomes (FAILED, REJECTED, NEEDS_HUMAN_REVIEW, and so on); the fix is scoped to transitions that are meant to mirror a successful expert sign-off.

Testing

Eleven new cases cover happy paths for each approved status, absence of runs for non-approved transitions, and upsert idempotency when a run already exists. The bulk status module’s test file now sits at eighty-three examples total—enough volume that regressions in the shortcut path are harder to sneak in.

The shared pattern

Both bugs come from the same structural mistake: a bypass that performed a subset of the steps the main flow performs automatically. Upload validation checked “is this extension globally OK?” but not “does it match the task’s original type?” Admin approval changed status but did not create the worker-run marker the rest of the system assumes exists after a human quality gate.

The normal path did both layers; the shortcut did one. Production did not complain at the boundary—it complained where assumptions accumulated: document jobs and read models that encoded those assumptions in SQL.

The lesson

Every shortcut in your system should document—or better, test—the same postconditions as the long path. If the full flow creates a worker run and updates status, the admin tool must do both. If the full flow ties revised files to original types, the upload API must enforce that match when it knows the original name, and the UI should steer users before the mistake ships.

When you add a bypass, ask explicitly: “What side effects did we skip?” Then either replay them in the shortcut, or weaken downstream queries so they do not rely on effects that are not guaranteed. The second option is rarer; usually you want the invariant, not a looser dashboard.

Invisible bugs are not magic. They are invariant drift between paths that look equivalent at the UI layer. Making those invariants explicit—and covered by tests—is how you surface them before a customer does.