fix(intake): stop a throttled sender being told their link is dead (#222)
Adding e2e specs for the submission page found a defect in the page they were written for, which is what they were for. One limiter counted page loads and submissions against the same twenty-per-quarter-hour allowance, so a sender working through a box of stock ran out after ten items — the exact person the feature exists for, and the exact case the limiter's own comment said must not be refused. The comment said refusing them costs a consignment while the number quietly did it. Worse, the page could not tell a 429 from a 404. `fetchIntakeLink` treated any non-OK response as "no link", so a throttled sender was told "This link is not active" and sent to ask for a replacement — which could not have helped, because the problem was their address and a minute of patience. Two conditions needing opposite reactions were sharing a message. Now two limiters, because the two requests cost different things. Reading a link hits one indexed row and writes nothing, so that allowance is generous at 120: someone re-reading the form or losing their signal should never be told to wait. Submitting writes up to six files, so that is the one worth bounding, at 30 — more than anyone photographing items can manage and far less than a script would want. The page gains a third state. Unknown, revoked and used-up still collapse into one "not active" card, because whether a link exists is not something a stranger needs to learn. Throttled is deliberately kept apart from them, since "wait a moment" and "go and ask for another link" are opposite instructions. Measured rather than assumed, on a freshly started process both times: before, 25 page loads produced 14 rejections; after, 40 produce none. The first attempt at that measurement was wrong and worth recording — the restart had failed with EADDRINUSE, so it read 30 of 30 against the old process's already-exhausted store. The two specs now pass in a full parallel run alongside everything else. They are scoped the way #241 asks: unique run ids, assertions naming only this run's rows, nothing asserted about the table as a whole. Backend: 284 integration, 309 unit. Frontend: build clean, lint unchanged at 2 pre-existing warnings. Ref #222, #241
This commit is contained in:
@@ -11,6 +11,7 @@ import Space from 'antd/es/space';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import { fetchIntakeLink, submitItem } from './intakeApi';
|
||||
import type { LinkState } from './intakeApi';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { TextArea } = Input;
|
||||
@@ -28,7 +29,7 @@ const MAX_IMAGES = 6;
|
||||
|
||||
export default function Submit() {
|
||||
const { token = '' } = useParams();
|
||||
const [label, setLabel] = useState<string | null>(null);
|
||||
const [state, setState] = useState<LinkState>({ kind: 'unusable' });
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [files, setFiles] = useState<UploadFile[]>([]);
|
||||
const [note, setNote] = useState('');
|
||||
@@ -38,11 +39,11 @@ export default function Submit() {
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void fetchIntakeLink(token).then((link) => {
|
||||
void fetchIntakeLink(token).then((result) => {
|
||||
// The token can change if the URL does, and a late response from the
|
||||
// previous one would otherwise overwrite the current answer.
|
||||
if (cancelled) return;
|
||||
setLabel(link?.label ?? null);
|
||||
setState(result);
|
||||
setChecking(false);
|
||||
});
|
||||
return () => {
|
||||
@@ -80,10 +81,28 @@ export default function Submit() {
|
||||
);
|
||||
}
|
||||
|
||||
// One state for every refusal, matching the server's single 404. Saying which
|
||||
// of revoked, unknown or used-up it was would tell a stranger whether a link
|
||||
// they guessed at exists.
|
||||
if (label === null) {
|
||||
// Kept apart from the state below on purpose. The two need opposite
|
||||
// reactions — wait a moment, versus go and ask for a different link — so
|
||||
// telling a throttled sender their link was dead would send them to fetch a
|
||||
// replacement that could not have helped.
|
||||
if (state.kind === 'throttled') {
|
||||
return (
|
||||
<div style={{ maxWidth: 640, margin: '48px auto', padding: '0 16px' }}>
|
||||
<Card>
|
||||
<Title level={3}>Too many requests just now</Title>
|
||||
<Paragraph>
|
||||
Your link is fine — this page has just been asked for too many times from your
|
||||
connection. Wait a minute and reload.
|
||||
</Paragraph>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// One state for unknown, revoked and used-up alike, matching the server's
|
||||
// single 404. Saying which it was would tell a stranger whether a link they
|
||||
// guessed at exists.
|
||||
if (state.kind === 'unusable') {
|
||||
return (
|
||||
<div style={{ maxWidth: 640, margin: '48px auto', padding: '0 16px' }}>
|
||||
<Card>
|
||||
|
||||
Reference in New Issue
Block a user