Your calm window into the Atmosphere. morgen.blue
rss atproto
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(auth): show inline errors on login instead of 500ing

Catch ConnectionException and RequestException from the Bluesky OAuth
redirect and convert them to ValidationException on `handle`. Inertia
now returns a 422 with the errors bag, so bad handles render under the
input and the spinner stops, instead of the request blowing up as a 500
and the spinner spinning forever.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+59 -8
+21 -8
app/Http/Controllers/Auth/AuthenticatedSessionController.php
··· 3 3 namespace App\Http\Controllers\Auth; 4 4 5 5 use App\Http\Controllers\Controller; 6 + use Illuminate\Http\Client\ConnectionException; 7 + use Illuminate\Http\Client\RequestException; 6 8 use Illuminate\Http\RedirectResponse; 7 9 use Illuminate\Http\Request; 8 10 use Illuminate\Support\Facades\Auth; 11 + use Illuminate\Validation\ValidationException; 9 12 use Inertia\Inertia; 10 13 use Inertia\Response; 11 14 use Laravel\Socialite\Facades\Socialite; ··· 27 30 $hint = $validated['handle'] ?? null; 28 31 $request->session()->put('atproto.hint', $hint); 29 32 30 - // Inertia submits the login form via XHR. Returning a Symfony redirect 31 - // would make the XHR follow the 302 cross-origin to the OAuth provider 32 - // and fail CORS. Inertia::location triggers a full browser navigation 33 - // instead via the X-Inertia-Location header. 34 - $redirect = Socialite::driver('bluesky') 35 - ->setScopes(explode(' ', (string) config('bluesky.oauth.metadata.scope'))) 36 - ->hint($hint) 37 - ->redirect(); 33 + try { 34 + // Inertia submits the login form via XHR. Returning a Symfony redirect 35 + // would make the XHR follow the 302 cross-origin to the OAuth provider 36 + // and fail CORS. Inertia::location triggers a full browser navigation 37 + // instead via the X-Inertia-Location header. 38 + $redirect = Socialite::driver('bluesky') 39 + ->setScopes(explode(' ', (string) config('bluesky.oauth.metadata.scope'))) 40 + ->hint($hint) 41 + ->redirect(); 42 + } catch (ConnectionException) { 43 + throw ValidationException::withMessages([ 44 + 'handle' => "Couldn't reach that account's server. Check the handle and try again.", 45 + ]); 46 + } catch (RequestException) { 47 + throw ValidationException::withMessages([ 48 + 'handle' => "That handle doesn't look right. Try your full handle, e.g. alice.bsky.social.", 49 + ]); 50 + } 38 51 39 52 return Inertia::location($redirect->getTargetUrl()); 40 53 }
+38
tests/Feature/Auth/LoginTest.php
··· 1 1 <?php 2 2 3 3 use App\Models\User; 4 + use GuzzleHttp\Psr7\Response as Psr7Response; 4 5 use Illuminate\Foundation\Testing\RefreshDatabase; 6 + use Illuminate\Http\Client\ConnectionException; 7 + use Illuminate\Http\Client\RequestException; 8 + use Illuminate\Http\Client\Response as HttpResponse; 5 9 use Laravel\Socialite\Facades\Socialite; 6 10 use Laravel\Socialite\Two\User as SocialiteUser; 7 11 use Revolution\Bluesky\Session\OAuthSession; ··· 27 31 ->assertRedirect('https://bsky.social/oauth/authorize'); 28 32 29 33 expect(session('atproto.hint'))->toBe('alice.bsky.social'); 34 + }); 35 + 36 + test('POST /login surfaces unreachable PDS as a handle validation error', function () { 37 + $provider = Mockery::mock(BlueskyProvider::class); 38 + $provider->shouldReceive('setScopes')->andReturnSelf(); 39 + $provider->shouldReceive('hint')->andReturnSelf(); 40 + $provider->shouldReceive('redirect')->andThrow( 41 + new ConnectionException('cURL error 6: Could not resolve host') 42 + ); 43 + 44 + Socialite::shouldReceive('driver')->with('bluesky')->andReturn($provider); 45 + 46 + $this->from(route('login')) 47 + ->post(route('login'), ['handle' => 'test.com']) 48 + ->assertRedirect(route('login')) 49 + ->assertSessionHasErrors(['handle']); 50 + }); 51 + 52 + test('POST /login surfaces auth-server rejection as a handle validation error', function () { 53 + $provider = Mockery::mock(BlueskyProvider::class); 54 + $provider->shouldReceive('setScopes')->andReturnSelf(); 55 + $provider->shouldReceive('hint')->andReturnSelf(); 56 + $provider->shouldReceive('redirect')->andThrow( 57 + new RequestException(new HttpResponse( 58 + new Psr7Response(400, [], '{"error":"invalid_request","error_description":"Invalid login_hint \"a\""}') 59 + )) 60 + ); 61 + 62 + Socialite::shouldReceive('driver')->with('bluesky')->andReturn($provider); 63 + 64 + $this->from(route('login')) 65 + ->post(route('login'), ['handle' => 'a']) 66 + ->assertRedirect(route('login')) 67 + ->assertSessionHasErrors(['handle']); 30 68 }); 31 69 32 70 test('OAuth callback creates user, stashes handle, and logs in', function () {