COR-03: fix verification failing on a member's first attempt

The insert into dsec_discord_members deserialised the response into
Vec<DiscordMemberRow>, but without `.returning(...)` supabase-lib-rs never sends
`Prefer: return=representation`, so PostgREST returns a 201 with an empty body.
Parsing that empty body failed and the `?` aborted before add_role and before
any reply — so every member's first click died with "This interaction failed",
while the row was still written (which is why the second attempt worked). Add
`.returning("student_id,discord_id")` so the row comes back and parses.

Also wrap the post-modal verification work so any failure sends the user an
ephemeral "something went wrong" instead of a dead interaction (poise's on_error
has no handle to the modal submission), and set an explicit on_error on
FrameworkOptions that replies ephemerally on command errors and keeps the
default logging. The database-write-first ordering is left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
This commit is contained in:
Clupai8o0 2026-08-30 15:40:01 +10:00
parent 14a2fa8c2f
commit 2b668388d5
2 changed files with 106 additions and 50 deletions

View file

@ -86,12 +86,18 @@ async fn add_dsec_discord_table(
"discord_id": member_id, "discord_id": member_id,
}); });
// `.returning(...)` makes supabase-lib-rs send `Prefer: return=representation`.
// Without it PostgREST defaults a POST to `return=minimal` — a 201 with an
// empty body — and deserialising that empty body into Vec<DiscordMemberRow>
// failed, aborting before the role grant even though the row was written.
// That is why verification failed on every member's first attempt (COR-03).
let _: Vec<DiscordMemberRow> = data let _: Vec<DiscordMemberRow> = data
.state .state
.supabase .supabase
.database() .database()
.insert("dsec_discord_members") .insert("dsec_discord_members")
.values(new_member)? .values(new_member)?
.returning("student_id,discord_id")
.execute() .execute()
.await?; .await?;
@ -220,9 +226,14 @@ async fn handle_verify(
return Ok(()); return Ok(());
}; };
// From here on we hold the modal-submit token, so the slower member fetch // From here on we hold the modal-submit token, so the slower member fetch and
// and database work below is no longer racing the button's ack window. // database work below is no longer racing the button's ack window. Wrap that
// work so any failure (e.g. a database error) still sends the user an
// ephemeral message rather than leaving a dead "This interaction failed"
// interaction — poise's on_error cannot reach this modal submission (COR-03).
let user_id = component_interaction.user.id; let user_id = component_interaction.user.id;
let verify_result: Result<(), Error> = async {
let discord_member = GuildId::member(guild_id, ctx, user_id).await?; let discord_member = GuildId::member(guild_id, ctx, user_id).await?;
let input_student_id = modal_data.student_id.to_lowercase(); let input_student_id = modal_data.student_id.to_lowercase();
@ -284,6 +295,28 @@ async fn handle_verify(
Ok(()) Ok(())
} }
.await;
if let Err(err) = verify_result {
eprintln!("[verify] verification failed after modal submit: {err}");
// Best-effort ephemeral error so the user does not see the generic
// "This interaction failed" with no way forward.
let _ = modal_submit
.create_response(
ctx,
ephemeral_embed(
CreateEmbed::new()
.title("Something went wrong")
.description(
"A maintainer has been notified. Please try again in a minute.",
),
),
)
.await;
}
Ok(())
}
pub async fn on_interaction_create( pub async fn on_interaction_create(
ctx: &Context, ctx: &Context,

View file

@ -126,6 +126,28 @@ async fn event_handler(
Ok(()) Ok(())
} }
/// Framework-level error handler. On a slash-command error it replies ephemerally
/// so the user is not left with a dead interaction, and it always runs poise's
/// default logging. `FrameworkOptions::default()` installs this default logger
/// too, but nothing surfaces it (no tracing subscriber, no log shipping — OPS-04),
/// so an explicit handler is set (COR-03).
async fn on_error(error: poise::FrameworkError<'_, Data, Error>) {
if let poise::FrameworkError::Command { ctx, .. } = &error {
let _ = ctx
.send(
poise::CreateReply::default()
.content(
"Something went wrong — a maintainer has been notified. Please try again in a minute.",
)
.ephemeral(true),
)
.await;
}
if let Err(e) = poise::builtins::on_error(error).await {
eprintln!("[on_error] failed while handling a framework error: {e}");
}
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
dotenv().ok(); // load env dotenv().ok(); // load env
@ -155,6 +177,7 @@ async fn main() {
event_handler: |ctx, event, framework, data| { event_handler: |ctx, event, framework, data| {
Box::pin(event_handler(ctx, event, framework, data)) Box::pin(event_handler(ctx, event, framework, data))
}, },
on_error: |error| Box::pin(on_error(error)),
..Default::default() ..Default::default()
}) })