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.

JobShared CodeException TypesCount
Jobs/Integrations/ImportClientWealthboxIntegrator, RedtailIntegrator, ImportsHousehold trait, SyncsPerson traitBadRequestHttpException12
Jobs/Integrations/AssignConfig, ImportsHouseholdTrait, LazyLoadsEntities, Orion\ConfigBadRequestHttpException, AccessDeniedHttpException, UnprocessableEntityHttpException, TooManyRequestsHttpException10
Jobs/Integrations/Syncs/SyncIntegrationOnNightlySync, all Integrators (Wealthbox, Redtail, SmartOffice)AccessDeniedHttpException, BadRequestHttpException20+
Jobs/Integrations/Syncs/SyncRootIntegrationMappingOnNightlySame as SyncIntegrationOnNightlySame20+
Jobs/Integrations/Syncs/SyncIntegrationOnAdHocLegacyApiBased ControllerUnprocessableEntityHttpException, AccessDeniedHttpException2
ListenerShared CodeException TypesCount
ClientPortalOpened/AutoSyncIntegrationsForHouseholdSync, all IntegratorsAccessDeniedHttpException, BadRequestHttpException20+
Billing/.../RemoveAdvisorFromOrganizationAdvisorBillingStrategy, AdvisorStrategyAccessDeniedHttpException6
Billing/.../SendLicenseInfoToNetworkConnectorWithIntegrationAccessDeniedHttpException1
FileLinesExceptionMessage
Wealthbox/Integrator.php130, 136, 140, 145BadRequestHttpExceptionHousehold/contact validation errors
Redtail/Integrator.php263, 267, 273BadRequestHttpExceptionContact/family validation errors
SmartOffice/Integrator.php49, 57, 81, 83, 85, 113, 148BadRequestHttpExceptionHousehold import validation
Support/ApiBased/Integrators/ImportsHousehold.php49, 51, 53, 84BadRequestHttpExceptionHead/spouse count validation
Support/Models/SyncsPerson.php119BadRequestHttpExceptionRole conflict
Support/LegacyApiBased/Configs/ImportsHouseholdTrait.php63, 70, 135, 137, 166, 168, 170BadRequestHttpException, AccessDeniedHttpExceptionHousehold linking validation
Support/LegacyApiBased/Configs/LazyLoadsEntities.php49BadRequestHttpExceptionMissing household ID
integrations-core/.../Config.php88, 145UnprocessableEntityHttpException, TooManyRequestsHttpExceptionUnmappable type, lock contention
integrations-core/.../Sync.php162AccessDeniedHttpExceptionAuthorization check
Orion/Config.php68AccessDeniedHttpExceptionDuplicate household link
Support/LegacyApiBased/Controllers/Controller.php60, 96UnprocessableEntityHttpException, AccessDeniedHttpExceptionCredential verification, duplicate integration
FileLinesExceptionMessage
Business/Support/Transfer/AdvisorBillingStrategy.php40, 48, 53, 58, 68AccessDeniedHttpExceptionBilling/license constraints
Business/Support/Transfer/AdvisorStrategy.php24AccessDeniedHttpExceptionNetwork 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 TypeOccurrencesAffected Jobs/Listeners
BadRequestHttpException~284
AccessDeniedHttpException~115
UnprocessableEntityHttpException32
TooManyRequestsHttpException11

All 44 throw points are in shared code. No job directly throws HTTP exceptions in its own code.