Skip to content

Improvement: HTTP Exceptions in Async Jobs

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.

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 lost

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.

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.

ProcessTaxReturnFile has a NotFoundHttpException in its call chain (HouseholdLocality::getHouseholdOrFail()), but it already wraps the entire handle() in try/catch(Throwable) and converts to RuntimeException. 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.

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
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
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
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

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);
}
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.