@internetarchive/ia-book-actions 1.4.6-alpha1 → 1.4.6-alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "author": "Internet Archive",
9
9
  "license": "AGPL-3.0-only",
10
- "version": "1.4.6-alpha1",
10
+ "version": "1.4.6-alpha2",
11
11
  "main": "index.js",
12
12
  "module": "index.js",
13
13
  "publishConfig": {
@@ -145,9 +145,13 @@ export default async function ActionsHandlerService(options) {
145
145
  * connection is exactly when this fires.
146
146
  */
147
147
  log(`[IABookActions] ✗ ${option.action} threw`, error);
148
+ // networkFailure marks "the service never answered" (like
149
+ // nonJsonResponse in parseServiceResponse) — see
150
+ // IABookActions.handleLendingActionError.
148
151
  option?.error?.({
149
152
  error: reachedResponse ? unexpectedError : networkError,
150
153
  detail: String(error),
154
+ ...(reachedResponse ? {} : { networkFailure: true }),
151
155
  });
152
156
  }
153
157
  }
@@ -1017,33 +1017,35 @@ export default class IABookActions extends LitElement {
1017
1017
  if (
1018
1018
  action === 'create_token' &&
1019
1019
  !isInitial &&
1020
- eventData?.nonJsonResponse === true &&
1021
- eventData?.status === 400
1020
+ (eventData?.nonJsonResponse === true ||
1021
+ eventData?.networkFailure === true)
1022
1022
  ) {
1023
- // Something in front of the service (the WAF's /about/400.html)
1024
- // answered a routine refresh instead of the service itself. It's
1025
- // already reported to Sentry (ActionsHandlerService/LoanTokenPoller);
1026
- // the current access token keeps the book readable, and the poll
1027
- // interval is left running, so the next tick tries again. No modal,
1028
- // and no session teardown.
1023
+ // The service never answered this routine refresh: something in
1024
+ // front of it did (the WAF's /about/400.html, an outage page), or the
1025
+ // request never got through. That says nothing about the loan, so
1026
+ // no modal and no session teardown — it's already reported to Sentry
1027
+ // (ActionsHandlerService/LoanTokenPoller), the current access token
1028
+ // keeps the book readable, and the poll interval is left running, so
1029
+ // the next tick tries again. The service's own answer (e.g. "You do
1030
+ // not currently have this book borrowed.") still falls through below.
1029
1031
  //
1030
1032
  // Not for the initial call: its success is what runs
1031
1033
  // lendingBarPostInit() and opens the book (see startLoanTokenPoller),
1032
1034
  // so suppressing it would leave a reader that never loads, silently.
1033
1035
  log(
1034
- '[IABookActions] create_token HTML 400 on a routine refresh — no modal',
1036
+ '[IABookActions] create_token routine refresh got no answer from the service — no modal',
1035
1037
  {
1036
1038
  identifier: this.identifier,
1037
1039
  status: eventData?.status,
1040
+ errorMsg,
1038
1041
  }
1039
1042
  );
1040
1043
  return;
1041
1044
  }
1042
1045
 
1043
1046
  if (action === 'create_token') {
1044
- // Any other create_token failure — initial or a routine interval
1045
- // refresh, except the routine HTML 400 above — means access can no
1046
- // longer be confirmed (e.g. a stale loan-record
1047
+ // Any other create_token failure — initial, or a routine refresh the
1048
+ // service itself answered — means access can no longer be confirmed (e.g. a stale loan-record
1047
1049
  // cache on some other node can make this request fail even though
1048
1050
  // the loan is fine, or the loan may genuinely have just been
1049
1051
  // returned via BookReaderImages.php's revokeAccess()). Don't retry
@@ -135,6 +135,7 @@ describe('ActionsHandlerService - thrown errors', () => {
135
135
 
136
136
  expect(error.calledOnce).to.be.true;
137
137
  expect(error.firstCall.args[0].error).to.equal(networkError);
138
+ expect(error.firstCall.args[0].networkFailure).to.be.true;
138
139
  const [, context] = window.Sentry.captureException.firstCall.args;
139
140
  expect(context.tags).to.deep.equal({
140
141
  identifier: 'foo',
@@ -158,5 +159,7 @@ describe('ActionsHandlerService - thrown errors', () => {
158
159
  expect(error.calledOnce).to.be.true;
159
160
  expect(error.firstCall.args[0].error).to.equal(unexpectedError);
160
161
  expect(error.firstCall.args[0].detail).to.contain('boom');
162
+ // the service did answer, so this isn't treated as "no answer"
163
+ expect(error.firstCall.args[0].networkFailure).to.be.undefined;
161
164
  });
162
165
  });
@@ -5,7 +5,10 @@ import Sinon from 'sinon';
5
5
 
6
6
  import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
7
7
  import { LocalCache } from '@internetarchive/local-cache';
8
- import { unexpectedResponseError } from '../src/core/services/actions-handler/actions-handler-service.js';
8
+ import {
9
+ unexpectedResponseError,
10
+ networkError,
11
+ } from '../src/core/services/actions-handler/actions-handler-service.js';
9
12
 
10
13
  beforeEach(() => {
11
14
  const modalManager = document.createElement('modal-manager');
@@ -1734,93 +1737,97 @@ describe('Sentry reports (WEBDEV-9595)', () => {
1734
1737
  });
1735
1738
  });
1736
1739
 
1737
- describe('handleLendingActionError - HTML 400 on create_token (WEBDEV-9595)', () => {
1740
+ describe('handleLendingActionError - routine create_token refresh failures (WEBDEV-9595)', () => {
1738
1741
  const htmlBadRequest = {
1739
1742
  success: false,
1740
1743
  error: unexpectedResponseError(400),
1741
1744
  status: 400,
1742
1745
  nonJsonResponse: true,
1743
1746
  };
1747
+ // the service never answered: no modal on a routine refresh
1748
+ const noAnswer = {
1749
+ 'an HTML 400 (WAF block)': htmlBadRequest,
1750
+ 'an HTML 502': {
1751
+ ...htmlBadRequest,
1752
+ error: unexpectedResponseError(502),
1753
+ status: 502,
1754
+ },
1755
+ 'a blocked or failed request': {
1756
+ error: networkError,
1757
+ detail: 'TypeError: Failed to fetch',
1758
+ networkFailure: true,
1759
+ },
1760
+ };
1761
+ // the service answered "no": the loan may really be gone
1762
+ const serviceAnswer = {
1763
+ success: false,
1764
+ error: 'You do not currently have this book borrowed.',
1765
+ status: 400,
1766
+ };
1744
1767
  const liveLoan = {
1745
1768
  is_lendable: true,
1746
1769
  user_has_borrowed: true,
1747
1770
  browsingExpired: false,
1748
1771
  secondsLeftOnLoan: 300,
1749
1772
  };
1750
-
1751
- it('shows no modal and keeps the session going on a routine refresh', async () => {
1773
+ const setup = async () => {
1752
1774
  const el = await fixture(
1753
1775
  container({
1754
1776
  userid: '@user1',
1755
- identifier: 'html-400-routine',
1777
+ identifier: 'token-refresh-failure',
1756
1778
  lendingStatus: liveLoan,
1757
1779
  })
1758
1780
  );
1759
1781
  await el.updateComplete;
1760
- const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1761
- const clearAllSpy = Sinon.spy(window.IALendingIntervals, 'clearAll');
1762
- el.tokenPoller = { disconnectedCallback: Sinon.spy() };
1782
+ return el;
1783
+ };
1763
1784
 
1764
- try {
1765
- el.handleLendingActionError({
1766
- detail: { action: 'create_token', isInitial: false, data: htmlBadRequest },
1767
- });
1785
+ for (const [name, data] of Object.entries(noAnswer)) {
1786
+ it(`shows no modal and keeps the session going for ${name}`, async () => {
1787
+ const el = await setup();
1788
+ const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1789
+ const clearAllSpy = Sinon.spy(window.IALendingIntervals, 'clearAll');
1790
+ el.tokenPoller = { disconnectedCallback: Sinon.spy() };
1791
+
1792
+ try {
1793
+ el.handleLendingActionError({
1794
+ detail: { action: 'create_token', isInitial: false, data },
1795
+ });
1796
+
1797
+ expect(showErrorModalSpy.called).to.be.false;
1798
+ // none of the create_token teardown ran
1799
+ expect(clearAllSpy.called).to.be.false;
1800
+ expect(el.tokenPoller.disconnectedCallback.called).to.be.false;
1801
+ expect(el.lendingStatus.secondsLeftOnLoan).to.equal(300);
1802
+ expect(el.lendingStatus.available_to_browse).to.be.undefined;
1803
+ } finally {
1804
+ clearAllSpy.restore();
1805
+ }
1806
+ });
1768
1807
 
1769
- expect(showErrorModalSpy.called).to.be.false;
1770
- // none of the create_token teardown ran
1771
- expect(clearAllSpy.called).to.be.false;
1772
- expect(el.tokenPoller.disconnectedCallback.called).to.be.false;
1773
- expect(el.lendingStatus.secondsLeftOnLoan).to.equal(300);
1774
- expect(el.lendingStatus.available_to_browse).to.be.undefined;
1775
- } finally {
1776
- clearAllSpy.restore();
1777
- }
1778
- });
1808
+ it(`still shows the modal for ${name} when the book is first opening`, async () => {
1809
+ const el = await setup();
1810
+ const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1779
1811
 
1780
- it('still shows the modal when the book is first opening', async () => {
1781
- const el = await fixture(
1782
- container({
1783
- userid: '@user1',
1784
- identifier: 'html-400-initial',
1785
- lendingStatus: liveLoan,
1786
- })
1787
- );
1788
- await el.updateComplete;
1789
- const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1812
+ el.handleLendingActionError({
1813
+ detail: { action: 'create_token', isInitial: true, data },
1814
+ });
1790
1815
 
1791
- el.handleLendingActionError({
1792
- detail: { action: 'create_token', isInitial: true, data: htmlBadRequest },
1816
+ expect(showErrorModalSpy.calledOnce).to.be.true;
1793
1817
  });
1818
+ }
1794
1819
 
1795
- expect(showErrorModalSpy.calledOnce).to.be.true;
1796
- });
1797
-
1798
- it("still shows the modal for the service's own JSON 400 and for an HTML 5xx", async () => {
1799
- const el = await fixture(
1800
- container({
1801
- userid: '@user1',
1802
- identifier: 'html-400-other',
1803
- lendingStatus: liveLoan,
1804
- })
1805
- );
1806
- await el.updateComplete;
1820
+ it("still shows the modal and resets the bar when the service itself says no", async () => {
1821
+ const el = await setup();
1807
1822
  const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1808
1823
 
1809
1824
  el.handleLendingActionError({
1810
- detail: {
1811
- action: 'create_token',
1812
- isInitial: false,
1813
- data: { error: 'You do not currently have this book borrowed.' },
1814
- },
1815
- });
1816
- el.handleLendingActionError({
1817
- detail: {
1818
- action: 'create_token',
1819
- isInitial: false,
1820
- data: { ...htmlBadRequest, status: 502 },
1821
- },
1825
+ detail: { action: 'create_token', isInitial: false, data: serviceAnswer },
1822
1826
  });
1823
1827
 
1824
- expect(showErrorModalSpy.calledTwice).to.be.true;
1828
+ expect(
1829
+ showErrorModalSpy.calledOnceWith(serviceAnswer.error, 'create_token')
1830
+ ).to.be.true;
1831
+ expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
1825
1832
  });
1826
1833
  });