Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class AwsHttpSession implements HttpSession {
private int maxInactiveInterval;
private long lastAccessedTime;
private boolean valid;
private boolean isNew = true;

/**
* @param id A unique session identifier
Expand All @@ -50,7 +51,7 @@ public AwsHttpSession(String id) {
}
this.id = id;
attributes = new HashMap<>();
creationTime = Instant.now().getEpochSecond();
creationTime = Instant.now().toEpochMilli();
maxInactiveInterval = SESSION_DURATION_SEC;
lastAccessedTime = creationTime;
valid = true;
Expand Down Expand Up @@ -118,18 +119,27 @@ public void invalidate() {

@Override
public boolean isNew() {
return lastAccessedTime == creationTime;
if (!valid) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[BUG] Throwing IllegalStateException from isNew() matches the spec in isolation, but it is the only accessor on this class that does so, and the request object keeps handing out invalidated sessions. AwsHttpServletRequest.getSession(boolean) caches the instance and never clears it on invalidation:

public HttpSession getSession(boolean b) {
    if (b && null == this.session) {
        AwsProxyRequestContext requestContext = (AwsProxyRequestContext) getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY);
        this.session = new AwsHttpSession(requestContext.getRequestId());
    }
    return this.session;
}

So after any framework code calls session.invalidate() (a logout handler, for example), a later request.getSession(false) still returns the dead session, and calling isNew() on it now throws an unchecked exception where it previously returned a boolean. Meanwhile getAttribute, getCreationTime and getLastAccessedTime continue to answer normally on the same invalidated object, so a caller has no non-throwing way to detect the invalid state (isValid() is package-private).

Two consistent options:

  • Apply the invalidation check across the accessors that the spec says must throw, and make getSession(false) return null (or create a fresh session for getSession(true)) once the cached session has been invalidated.
  • Or drop the new throw and keep isNew() total, leaving the invalidation contract out of this PR's scope.

Note also that isNew() throws only on explicit invalidate(); a session that has exceeded maxInactiveInterval has valid == true and so does not throw, even though isValid() reports false.

throw new IllegalStateException("Session is invalidated");
}
return isNew;
}

private void touch() {
lastAccessedTime = Instant.now().getEpochSecond();
lastAccessedTime = Instant.now().toEpochMilli();
isNew = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[BUG] Setting isNew = false inside touch() keeps the newness flag coupled to attribute access, which contradicts the HttpSession contract on both counts:

  • isNew() is defined as "returns true if the client does not yet know about the session or if the client chooses not to join the session" — it reflects whether the session id has round-tripped with the client, not whether the application read or wrote attributes.
  • getLastAccessedTime() is explicitly documented as unaffected by application actions: "Actions that your application takes, such as getting or setting a value associated with the session, do not affect the access time."

touch() is called from getAttribute, getAttributeNames, setAttribute and removeAttribute, so a purely read-only lookup now permanently flips the session out of "new" state.

This is also an observable behavior change relative to the pre-PR code. Previously isNew() compared second-granularity timestamps, so within a single Lambda invocation (sub-second in the overwhelming majority of cases) it stayed true for the whole request. Now the first framework-internal attribute read — e.g. a security filter probing for its context attribute — makes isNew() return false for the rest of the request. Given that AwsHttpServletRequest.getRequestedSessionId() returns null and isRequestedSessionIdValid() returns false, the client never joins the session, so true is the correct answer for the object's whole lifetime.

Since a new AwsHttpSession is created per request, the straightforward fix is to leave newness independent of attribute access:

private void touch() {
    lastAccessedTime = Instant.now().toEpochMilli();
}

and keep isNew true unless/until the container has a real notion of the client joining the session. If you instead intend to keep the current semantics, it is worth a comment explaining the deliberate divergence from the spec, because the field name suggests spec behavior.

}

boolean isValid() {
if (lastAccessedTime - creationTime < maxInactiveInterval) {
return valid;
} else {
if (!valid) {
return false;
}

if (maxInactiveInterval <= 0) {
return true;
}

return Instant.now().toEpochMilli() - lastAccessedTime
< maxInactiveInterval * 1000L;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ void new_withValidId_setsIdCorrectly() {

@Test
void new_creationTimePopulatedCorrectly() {
long beforeCreation = Instant.now().toEpochMilli();

AwsHttpSession session = new AwsHttpSession("id");
assertTrue(session.getCreationTime() > Instant.now().getEpochSecond() - 1);

long afterCreation = Instant.now().toEpochMilli();

assertTrue(session.getCreationTime() >= beforeCreation);
assertTrue(session.getCreationTime() <= afterCreation);
assertEquals(AwsHttpSession.SESSION_DURATION_SEC, session.getMaxInactiveInterval());
assertEquals(session.getLastAccessedTime(), session.getCreationTime());
}
Expand Down Expand Up @@ -64,16 +70,30 @@ void attributes_dataStoredCorrectly() throws InterruptedException {
}

@Test
void validSession_expectCorrectValidationOrInvalidation() throws InterruptedException {
void validSession_expectCorrectValidationOrInvalidation() {
AwsHttpSession sess = new AwsHttpSession("id");

assertTrue(sess.isValid());
assertTrue(sess.isNew());

Thread.sleep(1000);
sess.setAttribute("test", "test");

assertFalse(sess.isNew());

sess.invalidate();

assertFalse(sess.isValid());
assertNull(sess.getAttribute("test"));
assertThrows(IllegalStateException.class, sess::isNew);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[GENERAL] Replacing assertNull(sess.getAttribute("test")) with the isNew assertion removes the only coverage of invalidate() clearing the attribute map — no other test in this class (or elsewhere in the module) exercises attribute state after invalidation, so a regression in attributes.clear() would now go unnoticed. Keep both assertions:

sess.invalidate();

assertFalse(sess.isValid());
assertNull(sess.getAttribute("test"));
assertThrows(IllegalStateException.class, sess::isNew);

}

@Test
void nonPositiveMaxInactiveIntervalDoesNotExpireSession() {
AwsHttpSession sess = new AwsHttpSession("id");

sess.setMaxInactiveInterval(0);
assertTrue(sess.isValid());

sess.setMaxInactiveInterval(-1);
assertTrue(sess.isValid());
}
}