diff --git a/http-netty/src/main/java/io/micronaut/http/netty/cookies/NettyCookie.java b/http-netty/src/main/java/io/micronaut/http/netty/cookies/NettyCookie.java index 505a5cfca84..b2f88f84425 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/cookies/NettyCookie.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/cookies/NettyCookie.java @@ -34,7 +34,7 @@ @Internal public class NettyCookie implements Cookie { - private final io.netty.handler.codec.http.cookie.Cookie nettyCookie; + private transient io.netty.handler.codec.http.cookie.Cookie nettyCookie; /** * @param nettyCookie The Netty cookie @@ -170,4 +170,64 @@ public int compareTo(Cookie o) { NettyCookie nettyCookie = (NettyCookie) o; return nettyCookie.nettyCookie.compareTo(this.nettyCookie); } + + private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException { + out.writeUTF(nettyCookie.name()); + out.writeUTF(nettyCookie.value()); + out.writeLong(nettyCookie.maxAge()); + out.writeBoolean(nettyCookie.isHttpOnly()); + out.writeBoolean(nettyCookie.isSecure()); + + out.writeBoolean(nettyCookie.domain() != null); + if (nettyCookie.domain() != null) { + out.writeUTF(nettyCookie.domain()); + } + + out.writeBoolean(nettyCookie.path() != null); + if (nettyCookie.path() != null) { + out.writeUTF(nettyCookie.path()); + } + + io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite sameSite = null; + if (nettyCookie instanceof DefaultCookie defaultCookie) { + sameSite = defaultCookie.sameSite(); + } + out.writeBoolean(sameSite != null); + if (sameSite != null) { + out.writeUTF(sameSite.name()); + } + } + + private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException { + String name = in.readUTF(); + String value = in.readUTF(); + long maxAge = in.readLong(); + boolean httpOnly = in.readBoolean(); + boolean secure = in.readBoolean(); + + String domain = null; + if (in.readBoolean()) { + domain = in.readUTF(); + } + + String path = null; + if (in.readBoolean()) { + path = in.readUTF(); + } + + io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite sameSite = null; + if (in.readBoolean()) { + sameSite = io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite.valueOf(in.readUTF()); + } + + DefaultCookie newNettyCookie = new DefaultCookie(name, value); + newNettyCookie.setMaxAge(maxAge); + newNettyCookie.setHttpOnly(httpOnly); + newNettyCookie.setSecure(secure); + newNettyCookie.setDomain(domain); + newNettyCookie.setPath(path); + newNettyCookie.setSameSite(sameSite); + + this.nettyCookie = newNettyCookie; + } } diff --git a/reproduce_issue.groovy b/reproduce_issue.groovy new file mode 100644 index 00000000000..c34761c02df --- /dev/null +++ b/reproduce_issue.groovy @@ -0,0 +1,86 @@ +import groovy.transform.Field + +@Field +String testSource = """ +package io.micronaut.reproduce + +import io.micronaut.http.netty.cookies.NettyCookie +import io.netty.handler.codec.http.cookie.DefaultCookie +import java.io.ByteArrayOutputStream +import java.io.ObjectOutputStream +import java.io.NotSerializableException +import spock.lang.Specification + +class ReproduceIssueSpec extends Specification { + + def "NettyCookie should be serializable or throw NotSerializableException"() { + when: "a NettyCookie is created and serialization is attempted" + def nettyCookie = new NettyCookie(new DefaultCookie("mycookie", "myvalue")) + + boolean serializedSuccessfully = false + try { + def baos = new ByteArrayOutputStream() + def oos = new ObjectOutputStream(baos) + oos.writeObject(nettyCookie) + oos.close() + baos.close() + serializedSuccessfully = true + } catch (NotSerializableException e) { + // This is the specific exception that indicates the bug is reproduced. + println ">>> Caught NotSerializableException: " + e.message + serializedSuccessfully = false + } catch (Exception e) { + // Catch any other unexpected exceptions + println ">>> Caught unexpected exception during serialization: " + e.message + e.printStackTrace() + serializedSuccessfully = false + } + + then: "the NettyCookie can be serialized without NotSerializableException" + // This assertion should pass if the bug is fixed (serialization succeeds). + // This assertion should fail if the bug is reproduced (NotSerializableException is caught). + serializedSuccessfully == true + } +} +""" + +def testDir = new File("test-suite/src/test/groovy/io/micronaut/reproduce") +def testFile = new File(testDir, "ReproduceIssueSpec.groovy") + +try { + testDir.mkdirs() + testFile.write(testSource) + + // Command to execute the specific Spock test + def command = "./gradlew :test-suite:test --tests io.micronaut.reproduce.ReproduceIssueSpec" + println "Executing command: $command" + def process = command.execute() + process.waitForProcessOutput(System.out, System.err) + def gradlewExitCode = process.exitValue() + + // Logic to determine the final script exit code based on gradlew's exit code: + // If gradlewExitCode is 0: The Spock test passed. This means `serializedSuccessfully` was true. + // Thus, the bug is NOT reproduced (serialization succeeded). + // In this scenario, the outer script should exit with 0. + // + // If gradlewExitCode is non-zero (e.g., 1 for test failure): The Spock test failed. This means `serializedSuccessfully` was false. + // This indicates that `NotSerializableException` was caught, + // meaning the bug IS reproduced. + // In this scenario, the outer script should exit with 129. + + if (gradlewExitCode == 0) { + println "Gradle test passed. This implies NettyCookie serialized successfully. Bug is NOT reproduced." + System.exit(0) // Issue not reproduced + } else { + println "Gradle test failed. This implies NettyCookie serialization failed (likely NotSerializableException). Bug IS reproduced." + System.exit(129) // Issue reproduced + } +} catch (Exception e) { + e.printStackTrace() + System.exit(1) // Script execution error +} finally { + // Clean up the created test file and directory + if (testDir.exists()) { + testDir.deleteDir() // This will recursively delete the directory and its contents + } +} \ No newline at end of file