Skip to content

Fix silent error absorption in RSC payload injection#2486

Draft
AbanoubGhadban wants to merge 2 commits into2402-streaming-hang-on-errorfrom
abanoub/2450-inject-rsc-payload-error-logging
Draft

Fix silent error absorption in RSC payload injection#2486
AbanoubGhadban wants to merge 2 commits into2402-streaming-hang-on-errorfrom
abanoub/2450-inject-rsc-payload-error-logging

Conversation

@AbanoubGhadban
Copy link
Collaborator

Summary

  • Fixes silent error absorption in injectRSCPayload.ts where three error handlers (htmlStream.on('error', () => {}), catch {} in startRSC, and .catch(() => endResultStream())) swallowed errors, preventing them from reaching errorReporter/Sentry
  • Introduces a shared safePipe utility that replaces the scattered pipe+close boilerplate with a single function handling the Node.js pipe() gap (destination not ended on source destroy) and optional onError callback for non-fatal error reporting
  • Fixes a missed pipe site in transformRSCNodeStream.ts that PR Fix streaming renders hanging forever when errors occur during SSR #2407 did not cover

How it works

Errors emitted on resultStream inside injectRSCPayload propagate through safePipe's onError at the pipeToTransform site → emitError()readableStreamhandleStreamErrorerrorReporter. The pipe stays intact, data continues flowing, and Fastify never sees the errors.

Test plan

  • injectRSCPayload.test.ts — 7 tests pass
  • streamServerRenderedReactComponent.test.jsx — 16 tests pass
  • RSCRequestTracker.test.ts — 8 tests pass
  • streamBackpressure.e2e.test.tsx — 4 tests pass
  • handleStreamError.test.ts — 6 tests pass
  • streamErrorHang.test.ts — 3 tests pass
  • ESLint passes with 0 errors
  • Prettier formatting clean
  • TypeScript compilation clean

Closes #2450

🤖 Generated with Claude Code

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch abanoub/2450-inject-rsc-payload-error-logging

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 25, 2026

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.5 KB (0%)
react-on-rails/client bundled (gzip) (time) 62.5 KB (-0.01% 🔽)
react-on-rails/client bundled (brotli) 53.71 KB (0%)
react-on-rails/client bundled (brotli) (time) 53.71 KB (0%)
react-on-rails-pro/client bundled (gzip) 63.5 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 63.5 KB (0%)
react-on-rails-pro/client bundled (brotli) 54.67 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 54.67 KB (0%)
registerServerComponent/client bundled (gzip) 127.16 KB (0%)
registerServerComponent/client bundled (gzip) (time) 127.16 KB (0%)
registerServerComponent/client bundled (brotli) 61.54 KB (0%)
registerServerComponent/client bundled (brotli) (time) 61.54 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 121.69 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 121.69 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.63 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.63 KB (0%)

@claude
Copy link

claude bot commented Feb 25, 2026

PR Review: Fix silent error absorption in RSC payload injection

This PR tackles several important issues: silent error swallowing in the RSC streaming pipeline, concurrent upload race conditions, and generator fixes. The implementation is generally solid. Here are my findings:

Positive highlights

  • The safePipe abstraction cleanly consolidates the repeated pipe+close boilerplate across multiple call sites
  • Per-request UUID upload directories elegantly eliminate the shared-path race without needing a global lock
  • Path traversal protection via path.basename() is a good security addition
  • The barrier pattern in uploadRaceCondition.test.ts makes previously non-deterministic races fully deterministic — well-designed test infrastructure
  • Switching Promise.allPromise.allSettled in the upload handler is the right call to ensure in-flight copies complete before onResponse fires

Issues requiring attention

1. Double error emission in injectRSCPayload.ts (moderate)

When htmlStream emits an error, two handlers both emit on resultStream:

  1. The new htmlStream.on('error', (err) => { resultStream.emit('error', err); }) fires immediately
  2. finished(htmlStream) rejects → startRSC's catch block also emits the same error

The net effect is that every htmlStream error gets reported twice to errorReporter/Sentry. stream/promises' finished() always rejects when an 'error' event fires.

Fix options:

  • Remove the htmlStream.on('error', ...) handler and rely solely on finished() rejection in startRSC, or
  • Use a seen flag/WeakSet to deduplicate, or
  • Separate the concerns: use htmlStream.on('error', ...) for non-fatal reporting only and suppress it in the finished() rejection path

2. transformRSCNodeStream.tssafePipe without onError

return safePipe(stream as Readable, htmlExtractor);

The PR description says this fixes a missed pipe site from PR #2407, but without an onError callback, errors emitted by the RSC node stream still silently disappear — they won't reach errorReporter. The close-event fix is correct, but the error-visibility goal from PR #2407 is only half-achieved here.

3. rscWebpackConfig.js.tt — RSC loader appended to all resources (minor)

In the SWC branch's rscLoaderWrapper, the RSC loader { loader: 'react-on-rails-rsc/WebpackLoader' } is appended regardless of whether the current resource is actually a user component that needs it. In the original static-array branch, rule.use.push() has the same coverage. If this matches the existing behavior, it's fine — but worth confirming whether files like CSS modules or test utilities that happen to hit the JS rule are expected to go through the RSC loader.

4. Orphan cleanup uses mtime, not ctime (minor)

In master.ts, the orphan threshold check uses stats.mtimeMs. For upload directories, content is written once and never modified, so mtime and ctime are equivalent. However, for correctness (and to handle systems where mtime can be set backward), ctimeMs (inode change time) would be more reliable. Low impact given the 30-minute window.

5. addBarrier safety valve — potential false-positive test pass (minor)

The 10-second fallback resolve() means if fewer than expectedCount requests reach preHandler (e.g., one is rejected at validation), the barrier silently opens and the test continues. The test will then fail on assertions, but the error message won't clearly indicate the barrier didn't function. A counter assertion after the await would make failures clearer:

expect(arrived).toBe(expectedCount); // confirm the barrier was properly exercised

Summary

The core safePipe refactor and upload race fixes are solid. The double error emission in injectRSCPayload.ts is the most actionable issue — it would cause duplicate Sentry reports for every htmlStream error. The transformRSCNodeStream.ts gap means RSC stream errors remain invisible to the error reporter despite this PR's goal.

* termination is handled by the 'close' event below.
* Report errors on htmlStream by emitting them on resultStream, where they
* propagate to handleStreamError → errorReporter in the node renderer.
* Error alone is not the end of the stream — termination is handled by the
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double error emission risk

When htmlStream emits an error, finished(htmlStream) (called inside startRSC) will also reject with that same error. This means resultStream.emit('error', ...) gets called twice for a single htmlStream error — once here and once in the startRSC catch block — resulting in duplicate Sentry/errorReporter reports.

Consider removing this handler and relying solely on the startRSC catch block, or deduplicate using a seen-error guard:

let lastReportedErr: Error | undefined;
htmlStream.on('error', (err) => {
  const error = err instanceof Error ? err : new Error(String(err));
  if (error !== lastReportedErr) {
    lastReportedErr = error;
    resultStream.emit('error', error);
  }
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — this was a real double-emission bug. Fixed in eb254a4.

The htmlStream.on('error') handler now only emits on resultStream when startRSC hasn't been called yet (!rscPromise). Once startRSC is running, finished(htmlStream) in the startRSC catch block handles reporting — the handler still exists to prevent Node.js from throwing on unhandled 'error' event but no longer double-reports.

This correctly covers both edge cases:

  • Error before first data chunk (startRSC not running): htmlStream.on('error') reports it
  • Error after data starts flowing (startRSC running): finished() rejection → catch block reports it

@@ -55,5 +56,5 @@ export default function transformRSCStream(stream: NodeJS.ReadableStream): NodeJ
},
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This correctly adds the 'close'-event fix (the missed pipe site from PR #2407), but without an onError callback, errors emitted by the RSC node stream will still be silently dropped — they won't reach errorReporter.

If error visibility is desired here (consistent with the goal of this PR), consider:

return safePipe(stream as Readable, htmlExtractor, (err) => {
  // forward to caller or log — RSC stream errors should be observable
  htmlExtractor.emit('error', err);
});

Or at minimum document why silent error suppression is intentional for this specific pipe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The safePipe change here was specifically to fix the close event gap (the pipe() destination-not-ended-on-destroy issue that PR #2407 missed at this site). Error reporting for the RSC source stream is a pre-existing concern that this PR doesn't make worse.

Regarding the suggested fix — emitting on the destination (htmlExtractor) would actually break the pipe. In Node.js v20+, pipe() attaches an error handler on the destination that calls source.unpipe(destination), so destination.emit('error', err) would disconnect the pipe and stop data flow.

The proper way to add error reporting here would require changes to the caller (getReactServerComponent.server.ts) to provide an error callback, which is outside this PR's scope. The Transform's own try/catch in its transform callback already handles parse errors (line 53-54). If the source stream is destroyed, safePipe ensures htmlExtractor is properly ended via the close handler — React's createFromNodeStream sees the stream end and handles the incomplete payload via its error boundary.

Happy to open a follow-up issue for RSC source stream error reporting if desired.

@AbanoubGhadban
Copy link
Collaborator Author

Thanks for the thorough review! Addressed the findings:

Issue 1 (Double error emission) — Fixed in eb254a4. The htmlStream.on('error') handler now only emits when startRSC hasn't been called yet. Once running, finished() in startRSC's catch block handles reporting. The handler still exists to prevent unhandled error crashes.

Issue 2 (transformRSCNodeStream without onError) — Intentional. The safePipe change here fixes the close event gap (missed pipe site from PR #2407). The suggested htmlExtractor.emit('error', err) would break the pipe — Node.js pipe() attaches an error handler on the destination that calls unpipe(). RSC source stream error reporting is a pre-existing concern outside this PR's scope.

Issues 3, 4, 5 — These are about code in the base PR #2407 (rscWebpackConfig loader appending, orphan cleanup mtime vs ctime, barrier safety valve), not changes introduced by this PR. The diff reviewed appears to include the full range from the base branch's base rather than just this PR's changes.

@claude
Copy link

claude bot commented Feb 25, 2026

PR Review — Fix silent error absorption in RSC payload injection (#2486)

This PR bundles several well-scoped fixes into RC 4. The error-reporting improvements in injectRSCPayload.ts and the streaming pipeline clean-up are the headline changes; the concurrent-upload race-condition fixes and generator wiring fix are also solid. Overall the approach is sound. A few things worth addressing before merge:


  1. PATH-TRAVERSAL GUARD HAS A GAP (worker.ts)

path.basename("..") returns ".." on all platforms, so a filename of literally ".." passes the !safeFilename guard and produces:

path.join(uploadDir, "..") // → parent of the per-request upload dir

The file is then registered under savedFilePath pointing at the parent directory of the upload dir, and later copyUploadedAssets copies from that path to the bundle directory. On most filesystems writing to a directory path as a file will fail, but the path still escapes the intended sandbox. Even though this is an internal service, the fix is cheap:

if (!safeFilename || safeFilename === "." || safeFilename === "..") {
throw new Error("onFile: invalid filename: " + JSON.stringify(part.filename));
}


  1. setInterval IN master.ts IS NEVER STOPPED

The orphan-cleanup interval has no clearInterval and keeps a reference that prevents the event loop from exiting naturally. In the cluster master process that is usually fine at runtime, but it can cause test harnesses to hang and makes graceful shutdown harder to reason about.

Recommend storing the handle and calling .unref() so it never blocks exit:

const cleanupTimer = setInterval(() => { ... }, ORPHAN_CLEANUP_INTERVAL_MS);
cleanupTimer.unref(); // Do not prevent process exit


  1. DOUBLE ERROR-EMISSION WINDOW IN injectRSCPayload.ts (narrow but real)

The guard "if (!rscPromise)" in htmlStream.on("error", ...) is correct for the steady-state case, but there is a narrow window where both paths can emit:

  • htmlStream emits "error" after rscPromise is set but before finished(htmlStream) rejects on the same microtask flush.
  • The catch inside startRSC then also emits on resultStream.

This can produce two "error" events on resultStream, leading to double Sentry reports. A simple boolean flag (e.g. errorForwarded) alongside rscPromise would close the window completely.


  1. Promise.allSettled SILENTLY DISCARDS NON-FIRST FAILURES (handleNewBundlesProvided)

const firstFailure = settled.find((r) => r.status === "rejected");
if (firstFailure) throw firstFailure.reason;

If two bundles reject, the second rejection reason is swallowed. Consider logging all rejected reasons before rethrowing the first, or aggregating them into an AggregateError, to make multi-bundle failure debugging easier.


MINOR / NITS

  • Duplicate safePipe implementations: one in react-on-rails-pro/src/safePipe.ts and a slightly different one in react-on-rails-pro-node-renderer/src/shared/utils.ts. A short comment explaining why they cannot share an implementation would help future readers.

  • Test barrier safety valve: the setTimeout(resolve, 10_000) inside addBarrier resolves the gate even if only one request has arrived, silently negating the barrier. The test will still fail on assertion values, but the failure message will be confusing. A "if arrived < expectedCount" warning in the resolved path would make this clearer.

  • Credits removal: credits.md silently removes a GitHub profile link for Andy Wang. If intentional (dead link), fine; if accidental, restore it.


Overall this is high-quality work. The safePipe abstraction is cleaner than the scattered inline close-listeners it replaces, and the per-request UUID upload directory is the right fix for the race condition. Addressing items 1 and 2 above is recommended before tagging the release.

Comment on lines 191 to 194
// preventing path traversal attacks (e.g. filename "../../etc/shadow").
const safeFilename = path.basename(part.filename);
if (!safeFilename) {
throw new Error(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path-traversal guard has a gap: path.basename('..') returns '..' on all platforms, so a filename that is literally '..' passes this check. The resulting destinationPath would be the parent of this.uploadDir, and part.value.savedFilePath would point there too — meaning copyUploadedAssets later reads from the parent directory rather than an isolated file.

Add an explicit rejection for the dot-segment cases:

Suggested change
// preventing path traversal attacks (e.g. filename "../../etc/shadow").
const safeFilename = path.basename(part.filename);
if (!safeFilename) {
throw new Error(
const safeFilename = path.basename(part.filename);
if (!safeFilename || safeFilename === '.' || safeFilename === '..') {
throw new Error(
`onFile: received file with empty or invalid filename: ${JSON.stringify(part.filename)}`,
);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is about code in the base PR #2407 (worker.ts upload handling), not changes introduced by this PR. The diff reviewed includes the full range from the base branch's base rather than just this PR's single commit.


// Periodically clean up orphaned per-request upload directories that workers
// failed to remove (e.g. after a crash). Each worker creates uploads/<UUID>/
// directories that are normally cleaned up in the onResponse hook; this timer
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interval is never stopped and holds a strong reference to the event loop. In the cluster master process that is generally fine at runtime, but it prevents the process from exiting cleanly in test environments and complicates graceful-shutdown handling.

Call .unref() so the interval does not keep the process alive:

Suggested change
// directories that are normally cleaned up in the onResponse hook; this timer
const cleanupTimer = setInterval(() => {
void (async () => {

and after the setInterval(...) call:

  cleanupTimer.unref();

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — this is about code in the base PR #2407 (master.ts orphan cleanup), not changes introduced by this PR.

Comment on lines +294 to +298
* propagate to handleStreamError → errorReporter in the node renderer.
* Error alone is not the end of the stream — termination is handled by the
* 'close' event below.
*
* We only emit here when startRSC hasn't been called yet. Once startRSC is
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The !rscPromise guard correctly prevents double-emission in the common case, but there is a subtle window: if htmlStream emits error in the same microtask flush as the finished(htmlStream) rejection (both triggered by the same underlying stream error), the error event fires synchronously while rscPromise is already set, so this handler is skipped — good. However if the ordering is reversed (error event fires before finished() settles), the event handler emits here and then the startRSC catch block also emits. This is unlikely in practice but possible.

A boolean flag shared between the two code paths would be more robust:

let errorForwarded = false;
htmlStream.on('error', (err) => {
  if (!rscPromise && !errorForwarded) {
    errorForwarded = true;
    resultStream.emit('error', err instanceof Error ? err : new Error(String(err)));
  }
});

And in the startRSC catch block:

} catch (e) {
  if (!errorForwarded) {
    errorForwarded = true;
    resultStream.emit('error', e instanceof Error ? e : new Error(String(e)));
  }
  endResultStream();
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "subtle window" described here cannot occur due to Node.js's synchronous event handling guarantees:

  1. `rscPromise = startRSC()` is set synchronously when the first data chunk arrives (line 286)
  2. Inside `startRSC`, `finished(htmlStream)` registers its internal event listeners synchronously before the first `await` (line 261). There is no async gap between `rscPromise` being set and `finished()` listening.
  3. When `htmlStream` emits `'error'`, all registered listeners fire synchronously in registration order. Both our handler and `finished()`'s internal handler execute in the same synchronous call stack.

So the invariant holds: if `rscPromise` is set (our handler skips), `finished()` is guaranteed to be listening and will handle the error in the catch block. If `rscPromise` is not set, `startRSC` hasn't been called, so `finished()` isn't listening, and only our handler reports.

There is no ordering reversal possible — both the check (`!rscPromise`) and `finished()`'s listener registration happen synchronously before any event loop tick.

@AbanoubGhadban AbanoubGhadban force-pushed the 2402-streaming-hang-on-error branch from 436e7e3 to 59f02c0 Compare February 25, 2026 14:19
AbanoubGhadban and others added 2 commits February 25, 2026 16:21
Errors in injectRSCPayload (htmlStream errors, startRSC catch, rscPromise
catch) were silently swallowed, preventing them from reaching errorReporter
and Sentry. This change emits those errors on resultStream so they propagate
through handleStreamError to the error reporting infrastructure without
breaking the pipe or causing Fastify to return error responses.

Also introduces a shared safePipe utility that replaces the scattered
pipe+close boilerplate from PR #2407 with a single function that handles
the Node.js pipe() gap (destination not ended on source destroy) and
provides an optional onError callback for non-fatal error reporting.

Closes #2450

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When htmlStream errors after startRSC is running, both the
htmlStream.on('error') handler and finished(htmlStream) rejection in
startRSC's catch block would emit on resultStream, causing duplicate
error reports. Now the htmlStream error handler only emits when startRSC
hasn't been called yet; once running, finished() handles it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@AbanoubGhadban AbanoubGhadban force-pushed the abanoub/2450-inject-rsc-payload-error-logging branch from eb254a4 to 9621073 Compare February 25, 2026 14:21
@claude
Copy link

claude bot commented Feb 25, 2026

PR Review: Fix silent error absorption in RSC payload injection

Overall this is a well-motivated PR. The safePipe abstraction cleanly eliminates repeated pipe+close boilerplate, and fixing the three silent error-swallowing sites is the right call. A few things worth addressing before merging:


1. source.pipe() called before event listeners (safePipe.ts)

In safePipe.ts the pipe is set up first and event listeners are attached after:

source.pipe(destination);
if (typeof (source as Readable).on === 'function') {
  readableSource.on('error', onError);   // listeners attached after pipe
  readableSource.on('close', ...);
}

The sibling implementation in utils.ts does the opposite (listeners before pipe), which is the safer convention — avoids a theoretical race with synchronous error emission during pipe setup. Worth making these consistent.


2. Asymmetric error termination depending on rscPromise state

The error handler comment says "Error alone is not the end of the stream", but that is only true when rscPromise is null:

  • Before first HTML chunk (rscPromise === null): the htmlStream.on('error', ...) handler emits on resultStream and returns — the stream keeps running.
  • After first HTML chunk (rscPromise !== null): the handler does nothing, but stream/promises finished() rejects on any emitted 'error' event (not only on destroy()), so startRSC()'s catch block fires, emits the error, and calls endResultStream() — terminating the stream.

Non-fatal errors emitted without destroy() will silently stop the stream once rscPromise is set, contrary to the stated intent. If the goal is "report but keep streaming", the startRSC() catch path needs to distinguish fatal from non-fatal errors, or the error handler should remain active after rscPromise is set and use emitError-style reporting.


3. .catch() after .finally() is effectively unreachable for rscPromise rejections

In the htmlStream.on('end') handler:

rscPromise
  .then(cleanup)
  .finally(() => { rscRequestTracker.clear(); })
  .catch((e: unknown) => {
    resultStream.emit('error', ...);
    endResultStream();
  });

Because startRSC() is an async function with a top-level try/catch, rscPromise never rejects. The .catch() only fires if cleanup() or rscRequestTracker.clear() throws unexpectedly — not the documented intention. A short comment would prevent future readers from treating this as the primary error-recovery path for rscPromise failures.


4. No new tests for the error paths being fixed

The PR fixes three silent error-absorption sites but adds no error-scenario tests to injectRSCPayload.test.ts. Without tests that verify:

  • an error on htmlStream before the first HTML chunk propagates to resultStream
  • an error on htmlStream after the first HTML chunk propagates to resultStream
  • the startRSC() catch path actually emits on resultStream

it is difficult to be confident these fixes will not silently regress. All 7 existing tests cover only the happy path.


Minor nit

The comment on htmlStream.on('error', ...) says "finished(htmlStream) inside startRSC rejects on error" — worth noting that this is true for any emitted 'error' event (not only when the stream is destroyed), since that is the non-obvious part of stream/promises.finished's behaviour and directly relates to finding #2 above.

destination: T,
onError?: (err: Error) => void,
): T {
source.pipe(destination);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Listeners are attached after pipe(), whereas utils.ts's safePipe attaches them before calling pipe(). Prefer the before-pipe order to eliminate any theoretical race with a synchronous 'error' emission during piping:

Suggested change
source.pipe(destination);
if (typeof (source as Readable).on === 'function') {
const readableSource = source as Readable;
if (onError) {
readableSource.on('error', onError);
}
// 'close' fires after both normal 'end' and destroy().
// On normal end, pipe() already forwards 'end' to the destination — this is a no-op.
// On destroy, pipe() unpipes but does NOT end the destination — we do it here.
readableSource.on('close', () => {
if (!destination.writableEnded) {
destination.end();
}
});
}
source.pipe(destination);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant