Improvement: HTTP Exceptions in Async Jobs
Background
Section titled “Background”In retail-api, the exception handler (app/Exceptions/Handler.php) includes Symfony\Component\HttpKernel\Exception\HttpException in $internalDontReport. This means any HTTP exception thrown in an async Job is silently swallowed — never reported to Sentry, never logged.
Root Cause Chain
Section titled “Root Cause Chain”Job throws BadRequestHttpException → Handler::report() called → shouldntReport() checks $internalDontReport → HttpException::class is listed → returns true → report() returns early → parent::report() never called → Sentry reportable callback never invoked → Exception silently lostSecondary Safety Net
Section titled “Secondary Safety Net”Even if an HTTP exception bypasses $internalDontReport, SymfonyHttpExceptionIntegration (packages/libs/laravel-sentry/src/Integrations/Exceptions/ThirdParty/SymfonyHttpExceptionIntegration.php:39-41) downgrades all 4xx exceptions to Severity::debug(), which Sentry typically filters out at the project level.
Affected Jobs & Listeners
Section titled “Affected Jobs & Listeners”Category A: Jobs with Exclusive Code Paths
Section titled “Category A: Jobs with Exclusive Code Paths”No jobs found that directly throw HTTP exceptions in their own code. All HTTP exceptions originate from shared service code.
ProcessTaxReturnFilehas aNotFoundHttpExceptionin its call chain (HouseholdLocality::getHouseholdOrFail()), but it already wraps the entirehandle()intry/catch(Throwable)and converts toRuntimeException. No action needed.
Category B: Shared Code (Defer — Document Only)
Section titled “Category B: Shared Code (Defer — Document Only)”The following Jobs/Listeners are affected by HTTP exceptions thrown in shared service code (also used by HTTP Controllers). These require a broader refactoring strategy rather than per-job fixes.
Integration Jobs
Section titled “Integration Jobs”| Job | Shared Code | Exception Types | Count |
|---|---|---|---|
Jobs/Integrations/ImportClient | WealthboxIntegrator, RedtailIntegrator, ImportsHousehold trait, SyncsPerson trait | BadRequestHttpException | 12 |
Jobs/Integrations/Assign | Config, ImportsHouseholdTrait, LazyLoadsEntities, Orion\Config | BadRequestHttpException, AccessDeniedHttpException, UnprocessableEntityHttpException, TooManyRequestsHttpException | 10 |
Jobs/Integrations/Syncs/SyncIntegrationOnNightly | Sync, all Integrators (Wealthbox, Redtail, SmartOffice) | AccessDeniedHttpException, BadRequestHttpException | 20+ |
Jobs/Integrations/Syncs/SyncRootIntegrationMappingOnNightly | Same as SyncIntegrationOnNightly | Same | 20+ |
Jobs/Integrations/Syncs/SyncIntegrationOnAdHoc | LegacyApiBased Controller | UnprocessableEntityHttpException, AccessDeniedHttpException | 2 |
Queued Listeners
Section titled “Queued Listeners”| Listener | Shared Code | Exception Types | Count |
|---|---|---|---|
ClientPortalOpened/AutoSyncIntegrationsForHousehold | Sync, all Integrators | AccessDeniedHttpException, BadRequestHttpException | 20+ |
Billing/.../RemoveAdvisorFromOrganization | AdvisorBillingStrategy, AdvisorStrategy | AccessDeniedHttpException | 6 |
Billing/.../SendLicenseInfoToNetwork | ConnectorWithIntegration | AccessDeniedHttpException | 1 |
Shared Code Locations (Reference)
Section titled “Shared Code Locations (Reference)”Integration Module
Section titled “Integration Module”| File | Lines | Exception | Message |
|---|---|---|---|
Wealthbox/Integrator.php | 130, 136, 140, 145 | BadRequestHttpException | Household/contact validation errors |
Redtail/Integrator.php | 263, 267, 273 | BadRequestHttpException | Contact/family validation errors |
SmartOffice/Integrator.php | 49, 57, 81, 83, 85, 113, 148 | BadRequestHttpException | Household import validation |
Support/ApiBased/Integrators/ImportsHousehold.php | 49, 51, 53, 84 | BadRequestHttpException | Head/spouse count validation |
Support/Models/SyncsPerson.php | 119 | BadRequestHttpException | Role conflict |
Support/LegacyApiBased/Configs/ImportsHouseholdTrait.php | 63, 70, 135, 137, 166, 168, 170 | BadRequestHttpException, AccessDeniedHttpException | Household linking validation |
Support/LegacyApiBased/Configs/LazyLoadsEntities.php | 49 | BadRequestHttpException | Missing household ID |
integrations-core/.../Config.php | 88, 145 | UnprocessableEntityHttpException, TooManyRequestsHttpException | Unmappable type, lock contention |
integrations-core/.../Sync.php | 162 | AccessDeniedHttpException | Authorization check |
Orion/Config.php | 68 | AccessDeniedHttpException | Duplicate household link |
Support/LegacyApiBased/Controllers/Controller.php | 60, 96 | UnprocessableEntityHttpException, AccessDeniedHttpException | Credential verification, duplicate integration |
Business Module
Section titled “Business Module”| File | Lines | Exception | Message |
|---|---|---|---|
Business/Support/Transfer/AdvisorBillingStrategy.php | 40, 48, 53, 58, 68 | AccessDeniedHttpException | Billing/license constraints |
Business/Support/Transfer/AdvisorStrategy.php | 24 | AccessDeniedHttpException | Network membership constraint |
Recommended Fix Strategy
Section titled “Recommended Fix Strategy”Short-term: Job-level Guard (Per-job)
Section titled “Short-term: Job-level Guard (Per-job)”Add a try/catch in each affected Job’s handle() to catch HttpException and convert:
use Symfony\Component\HttpKernel\Exception\HttpException;
public function handle(): void{ try { // existing logic } catch (HttpException $e) { throw new \RuntimeException($e->getMessage(), $e->getStatusCode(), $e); }}This ensures the exception is reported to Sentry while preserving the original message and context.
Long-term: Domain Exceptions in Shared Code
Section titled “Long-term: Domain Exceptions in Shared Code”Refactor shared Integration/Business code to throw domain-specific exceptions instead of HTTP exceptions. Controllers can then catch domain exceptions and convert to HTTP responses, while Jobs let them propagate naturally.
// Before (coupled to HTTP layer)throw new BadRequestHttpException('The household is invalid.');
// After (domain exception)throw new InvalidHouseholdException('The household is invalid.');
// Controller layer converts:catch (InvalidHouseholdException $e) { throw new BadRequestHttpException($e->getMessage(), $e);}Summary Stats
Section titled “Summary Stats”| Exception Type | Occurrences | Affected Jobs/Listeners |
|---|---|---|
BadRequestHttpException | ~28 | 4 |
AccessDeniedHttpException | ~11 | 5 |
UnprocessableEntityHttpException | 3 | 2 |
TooManyRequestsHttpException | 1 | 1 |
All 44 throw points are in shared code. No job directly throws HTTP exceptions in its own code.