Fix silent message corruption from absent/blank channel scripts (#344)#351
Fix silent message corruption from absent/blank channel scripts (#344)#351e210 wants to merge 3 commits into
Conversation
OpenIntegrationEngine#344) Signed-off-by: Ezio Caffi <ezio.caffi@gmail.com>
Signed-off-by: Ezio Caffi <ezio.caffi@gmail.com>
c0fd2d2 to
7642566
Compare
jonbartels
left a comment
There was a problem hiding this comment.
Holding my review until I hear from the original issue reporter - https://discord.com/channels/943670759891554316/943670760461987862/1528943487163044094
Looks good at a glance, but I'll let the OP verify it first
|
Thanks for tracking this down — clean fix, and the root cause explains a symptom I couldn't (RAW channels corrupting too, since the preprocessor runs on the raw message before datatype handling). Nice test coverage on the real Rhino path. |
There was a problem hiding this comment.
Pull request overview
Fixes silent message corruption caused by absent/blank channel preprocessor scripts by ensuring such scripts aren’t cached/treated as custom, and by preventing Rhino Undefined results from being coerced into the literal "undefined" message payload.
Changes:
- Update preprocessor execution to ignore Rhino
Undefinedreturn values for both global and channel preprocessors. - Update
compileAndAddScriptto treat null/blank preprocessor scripts as absent (cache eviction + no compile). - Add Rhino-path unit tests covering null/blank script compilation behavior and
Undefinedhandling.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
server/src/main/java/com/mirth/connect/server/util/javascript/JavaScriptUtil.java |
Prevents Undefined from corrupting messages and adjusts script compilation/caching behavior for absent scripts. |
server/src/test/java/com/mirth/connect/server/util/javascript/JavaScriptUtilTest.java |
Adds tests to reproduce/guard the null/blank script + Undefined scenarios using real Rhino execution paths. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ConfigurationController configurationController = mock(ConfigurationController.class); | ||
| when(controllerFactory.createConfigurationController()).thenReturn(configurationController); | ||
|
|
||
| ExtensionController extensionController = mock(ExtensionController.class); | ||
| when(controllerFactory.createExtensionController()).thenReturn(extensionController); | ||
|
|
||
| CodeTemplateController codeTemplateController = mock(CodeTemplateController.class); | ||
| when(controllerFactory.createCodeTemplateController()).thenReturn(codeTemplateController); |
There was a problem hiding this comment.
Mockito's default answer (ReturnsEmptyValues) returns empty collections — not null — for getCodeTemplates()/getLibraries()/getPluginMetaData(), so appendCodeTemplates() and generateGlobalSealedScript() no-op rather than NPE. The full suite (659 tests, real class ordering) runs green in CI. The @BeforeClass repair covers the ordering concern — now via the setControllersForTesting method added in 42d4c1f.
| // A null or blank script does nothing; treat it as absent instead of | ||
| // compiling a wrapper whose body is the literal string "null" (MIRTH/OIE #344) | ||
| if (StringUtils.isBlank(script)) { | ||
| compiledScriptCache.removeCompiledScript(scriptId); | ||
| return false; | ||
| } |
There was a problem hiding this comment.
True in principle, but the only affected configs were already broken: a blank select script cannot be saved from the Administrator (REST-only — which is how this whole issue started). Before this PR every poll threw DatabaseReceiverException("…: undefined"); now it is an NPE from the same throw line, caught by the same catch. Same loud failure either way. Scoping the behavior per-context and compiling empty wrappers elsewhere would reintroduce the phantom-script executions this PR removes. Happy to add a null-guard to that error message in a follow-up if wanted.
|
Checked both Copilot notes: Test mocks: Mockito's default returns empty collections, not null — so no NPE, the loops just no-op. The 659 green tests in CI agree. Blank scripts ( |
tonygermano
left a comment
There was a problem hiding this comment.
See inline comments. Also, please respond to the co-pilot suggestions.
| } | ||
|
|
||
| if (result != null) { | ||
| if (result != null && !(result instanceof Undefined)) { |
There was a problem hiding this comment.
This is the correct way to test for javascript undefined in java. There is one other incorrect usage in this file on line 333 which is not currently a part of this PR, but it would be great if you could fix that one, too, for consistency.
| if (result != null && !(result instanceof Undefined)) { | |
| if (result != null && !(Undefined.isUndefined(result))) { |
There was a problem hiding this comment.
Applied in 42d4c1f, including line 333 — and a fourth occurrence of the same pattern at line 132 (attachment script result handling), fixed for the same consistency reason.
| } | ||
|
|
||
| if (result != null) { | ||
| if (result != null && !(result instanceof Undefined)) { |
There was a problem hiding this comment.
| if (result != null && !(result instanceof Undefined)) { | |
| if (result != null && !(Undefined.isUndefined(result))) { |
| * mirth.properties isn't on the unit test classpath, but JavaScriptScopeUtil's static init | ||
| * requires it to be resolvable via the context classloader. Point the context classloader | ||
| * at the real conf/ dir; restored after the class. | ||
| */ | ||
| originalContextClassLoader = Thread.currentThread().getContextClassLoader(); | ||
| URL confDir = new File("conf").toURI().toURL(); |
There was a problem hiding this comment.
suggestion: I would prefer to see a copy of mirth.properties added as a test resource
Accessing Files directly from tests can be flaky depending on where the test is run from, and this file being referenced is not guaranteed to be static.
There was a problem hiding this comment.
Done in 42d4c1f — mirth.properties copied to test resources, classloader shim removed entirely.
| * earlier test class loaded it with a mocked factory that left them null, repair them so | ||
| * generateGlobalSealedScript/appendCodeTemplates don't NPE. | ||
| */ | ||
| setJavaScriptBuilderStaticField("extensionController", extensionController); |
There was a problem hiding this comment.
suggestion: create package-private method for changing these values for testing
It seems a bit hacky to use reflection to change these values, but I understand why you did it. Refactoring JavaScriptBuilder to allow injectable dependencies is probably out of scope for this PR, but I think adding a method would be an acceptable compromise.
// In JavaScriptBuilder.java (from Guava, which should already be on the classpath)
import com.google.common.annotations.VisibleForTesting;
@VisibleForTesting
static void setControllersForTesting(ExtensionController ec, CodeTemplateController ctc) {
extensionController = ec;
codeTemplateController = ctc;
}There was a problem hiding this comment.
Done in 42d4c1f — one tweak: the method had to be public rather than package-private, since the test lives in server.util.javascript while the builder is in server.builders. @VisibleForTesting marks the intent.
…rationEngine#351) - Use Undefined.isUndefined() instead of instanceof, which also covers SCRIPTABLE_UNDEFINED; apply the same idiom to the postprocessor and attachment-script result handling for consistency - Load mirth.properties from test resources instead of pointing the context classloader at conf/ - Replace reflection on JavaScriptBuilder's static controllers with a @VisibleForTesting setter (public: the test lives in a different package) Signed-off-by: Ezio Caffi <ezio.caffi@gmail.com>
Fixes the root cause behind #344:
POST /api/channelsaccepts a channel without<preprocessingScript>; the channel deploys "healthy" but corrupts every message to the literal stringundefinedon the wire.Root cause
Two independent defects in
JavaScriptUtil(full analysis in #344):JavaScriptBuilder.generateScriptstring-concatenates anullscript into the JS wrapper, producingfunction doScript() { null }.compileAndAddScriptcaches it as a "custom" preprocessor because its body differs from the default script.executePreprocessorScriptsguards results withresult != null, but Rhino'sUndefinedis not Javanull—Context.jsToJavacoerces it to the string"undefined", which becomes the processed raw and flows to every destination.Changes
compileAndAddScript: a null/blank script is now treated as absent — evict from the compiled-script cache and returnfalse(all callers already handlefalseas "no script").executePreprocessorScripts: both result checks (global + channel) now ignoreUndefined, the same idiomgetPostprocessorResponsealready uses.JavaScriptUtilTest: 5 tests exercising the real Rhino path (TDD — each fix developed against a failing test).Behavior change note
A user preprocessor with no
returnstatement previously corrupted the message body to"undefined"; it now passes the message through unchanged. This is footgun-removal: returning the literal string"undefined"as content is never intentional (return '';andreturn null;behave as before).Verification
:server:testsuite green.\x0bundefined\x1c\rThe broader ask in #344 — structural validation/normalization in
POST /api/channels— is deliberately out of scope here; it deserves its own design discussion. This PR removes the data corruption.