Warn logging with "Failed to mark a promise as success because it is done already: DefaultChannelPromise" after upgrading to 4.0.17.Final #2302

Closed
twilek opened this Issue Mar 11, 2014 · 15 comments

Projects

None yet

7 participants

@twilek
twilek commented Mar 11, 2014

This may not be a bug, but I have a very hard time figuring out what is wrong. After upgrading from 4.0.15.Final to 4.0.17.Final I now get quite many warn messages in my log with the message "Failed to mark a promise as success because it is done already: DefaultChannelPromise" in my production environment. The server seems to work fine both before and after upgrade and I do not get any stack where this error is triggerd and can not really see what is wrong and how to get rid of this warning. Any suggestions?

@normanmaurer
Member

Can you show me the full log ?

@twilek
twilek commented Mar 11, 2014

A lot of rows like this

2014-03-11 10:54:39 WARN  ChannelOutboundBuffer:609 - Failed to mark a promise as success because it is done already: DefaultChannelPromise@5e15e68d(failure(io.netty.handler.codec.EncoderException: io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1)
@normanmaurer
Member

@twilek interesting. Can you show me the ChannelPipeline and let me know what ChannelHandlers you have placed in? If you have any "self-written" ChannelHandlers please also include the code of them or send the code to me if you can't make it public.

@normanmaurer normanmaurer added this to the 4.0.18.Final milestone Mar 11, 2014
@twilek
twilek commented Mar 12, 2014

Here is the ChannelInitializer that creates the pipline

public class HttpServerInitializer extends ChannelInitializer<SocketChannel> implements ServerInitializer {

    private final Router router;
    private final ResponseWriter responseWriter;
    private final int maxContentLength;

    public HttpServerInitializer(ServerConfig config) {
        this.maxContentLength = config.getMaxContentLength();
        this.router = new HttpRouter();
        this.responseWriter = new ResponseWriter();
    }

    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast("decoder", new HttpRequestDecoder());
        pipeline.addLast("aggregator", new HttpObjectAggregator(maxContentLength));
        pipeline.addLast("encoder", new HttpResponseEncoder());
        pipeline.addLast("handler", new HttpServerHandler(router, responseWriter));
    }

    @Override
    public ChannelHandler getChildHandler() {
        return this;
    }

    @Override
    public Router getRouter() {
        return router;
    }
}

This have a self-written handler that looks like this

public class HttpServerHandler extends SimpleServerHandler<FullHttpRequest> {
    public HttpServerHandler(Router router,
                             ResponseWriter responseWriter) {
        super(router, responseWriter);
    }

    @Override
    protected Request createRequest(FullHttpRequest message, ChannelHandlerContext context) {
        return new HttpRequest(message, context);
    }
}

Where HttpRequest basically is a facade around FullHttpRequest and HttpServerHandler inherits from

public abstract class SimpleServerHandler<T> extends SimpleChannelInboundHandler<T> {

    private final Router router;
    private final ResponseWriter responseWriter;

    protected SimpleServerHandler(Router router,
                                  ResponseWriter responseWriter) {
        this.router = router;
        this.responseWriter = responseWriter;
    }

    protected abstract Request createRequest(T message, ChannelHandlerContext context);

    @Override
    @SuppressWarnings("unchecked")
    public void channelRead0(ChannelHandlerContext context, T message) throws Exception {
        Request request = createRequest(message, context);
        Response response = router.handle(request);
        responseWriter.write(context, response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
        context.channel().close();
    }

}

The SimpleServerHandler also make use of the response writer that looks like this

@Log4j
public class ResponseWriter {

    public void write(ChannelHandlerContext context, Response response) {
        logException(response);
        ChannelFuture future = null;
        for(Object message : response.finalizeMessageList()){
            future = context.write(message);
        }
        context.flush();
        if((future != null) && !response.isKeepAlive()){
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }

    private void logException(Response response) {
        if(!response.isSuccessful()){
            String message = getErrorMessage(response);
            log.log(response.causeSeverity(), message, response.cause());
        }
    }

    private static String getErrorMessage(Response response) {
        String path = response.getRequest().getPath();
        String statusMessage = response.getStatusMessage();
        return String.format("Failed to handle request %s. Status: %s", path, statusMessage);
    }

}

The response that is created looks like this

public class HttpResponse extends Response<FullHttpResponse> {
    private static final String DEFAULT_CONTENT_TYPE = "text/plain; charset=UTF-8";
    private HttpResponseStatus status;
    private HttpHeaders headers;
    private boolean forwardCause;

    public HttpResponse(Request request) {
        super(request);
    }

    public HttpHeaders headers() {
        if(headers == null){
            headers = new DefaultHttpHeaders();
            headers.set(CONTENT_TYPE, DEFAULT_CONTENT_TYPE);
        }
        return headers;
    }

    @Override
    public String getStatusMessage() {
        return (forwardCause && (cause() != null)) ? cause().getMessage() : getHttpStatus().toString();
    }

    @Override
    public FullHttpResponse createMessage() {
        FullHttpResponse fullResponse = new DefaultFullHttpResponse(HTTP_1_1, getHttpStatus(), getContent());
        fullResponse.headers().set(headers());
        fullResponse.headers().set(CONNECTION, HttpHeaders.Values.CLOSE);
        if (isRedirected()) {
            fullResponse.headers().set(LOCATION, getRedirect());
        } else if (isKeepAlive()) {
            fullResponse.headers().set(CONTENT_LENGTH, fullResponse.content().readableBytes());
            fullResponse.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
        }
        return fullResponse;
    }

    public HttpResponseStatus getHttpStatus() {
        if (status != null) {
            return status;
        }
        return isRedirected() ? FOUND : OK;
    }

    public void setHttpStatus(HttpResponseStatus responseStatus) {
        this.status = responseStatus;
    }

    public void setHttpStatus(HttpResponseStatus responseStatus, Throwable cause) {
        setHttpStatus(responseStatus, cause, Level.DEBUG);
    }

    public void setHttpStatus(HttpResponseStatus responseStatus, Throwable cause, Level severity) {
        setHttpStatus(responseStatus, cause, severity, false);
    }

    public void setHttpStatus(HttpResponseStatus responseStatus, Throwable cause, boolean forwardCause) {
        setHttpStatus(responseStatus, cause, Level.DEBUG, forwardCause);
    }

    public void setHttpStatus(HttpResponseStatus responseStatus,
                              Throwable cause,
                              Level severity,
                              boolean forwardCause) {
        this.forwardCause = forwardCause;
        setHttpStatus(responseStatus);
        setCause(cause, severity);
    }

}

Which inherits from

public abstract class Response<T> {

    private final List<Object> messageList;

    private static final ByteBuf EMPTY_BUFFER = Unpooled.buffer(0);
    private String redirect;
    private final Request request;
    private Throwable cause;
    private ByteBuf content = EMPTY_BUFFER;
    private boolean keepAlive;
    private Level severity = Level.DEBUG;

    protected Response(Request request) {
        this.request = request;
        this.messageList = new ArrayList<Object>();
        this.keepAlive = true;
    }
    public abstract T createMessage();

    public abstract String getStatusMessage();

    public List<Object> messageList() {
        return messageList;
    }

    public List<Object> finalizeMessageList() {
        if(messageList().isEmpty()){
            messageList().add(createMessage());
        }
        return messageList();
    }

    public ByteBuf getContent() {
        if (isSuccessful()) {
            return content;
        }
        return Unpooled.copiedBuffer(getStatusMessage(), CharsetUtil.UTF_8);
    }

    public void setContent(byte[] data) {
        this.content = Unpooled.copiedBuffer(data);
    }

    public void setContent(ByteBuf data) {
        this.content = data;
    }

    public Request getRequest() {
        return request;
    }

    public String getRedirect() {
        return redirect;
    }

    public void setRedirect(String redirect) {
        this.redirect = redirect;
    }

    public boolean isRedirected() {
        return this.redirect != null;
    }

    public boolean isKeepAlive() {
        return keepAlive && getRequest().isKeepAlive() && isSuccessful() && !isRedirected();
    }

    public void setKeepAlive(boolean keepAlive){
        this.keepAlive = keepAlive;
    }

    public boolean isSuccessful() {
        return cause == null;
    }

    public Throwable cause() {
        return cause;
    }

    public Level causeSeverity() {
        return severity;
    }

    public void setCause(Throwable cause) {
        setCause(cause, Level.DEBUG);
    }

    public void setCause(Throwable cause, Level severity) {
        this.cause = cause;
        this.severity = severity;
    }

}

I think that shall be the most essenial parts of the code. If you need anything more. Tell me.

@normanmaurer
Member

@twilek could you please try to move the HttpResponseEncoder to be the first ChannelHandler in your ChannelPipeline?

@twilek
twilek commented Mar 31, 2014

Sorry for not answering until now but I have been away for two weeks. I'll try this now and return when I know if this will change anything.

@normanmaurer
Member

@twilek thanks!

@twilek
twilek commented Apr 1, 2014

Hmm, when I move the HttpResponseEncoder to be the first ChannelHandler the server hangs and do not respond and do not even hit any of my code. This is what is shown in the log

14:34:19,135 [main] INFO DefaultLoggingInitializer:29 - Application is running in developmentMode
14:34:19,147 [main] INFO HttpServer:17 - Starting HTTP Server on port 8082
14:34:19,154 [main]DEBUG MultithreadEventLoopGroup:41 - -Dio.netty.eventLoopThreads: 8
14:34:19,160 [main]DEBUG PlatformDependent:391 - Platform: Windows
14:34:19,160 [main]DEBUG PlatformDependent:522 - Java version: 6
14:34:19,160 [main]DEBUG PlatformDependent:529 - -Dio.netty.noUnsafe: false
14:34:19,163 [main]DEBUG PlatformDependent0:61 - java.nio.ByteBuffer.cleaner: available
14:34:19,164 [main]DEBUG PlatformDependent0:80 - java.nio.Buffer.address: available
14:34:19,164 [main]DEBUG PlatformDependent0:88 - sun.misc.Unsafe.theUnsafe: available
14:34:19,164 [main]DEBUG PlatformDependent0:98 - sun.misc.Unsafe.copyMemory: available
14:34:19,164 [main]DEBUG PlatformDependent0:135 - java.nio.Bits.unaligned: true
14:34:19,165 [main]DEBUG PlatformDependent:556 - sun.misc.Unsafe: available
14:34:19,165 [main]DEBUG PlatformDependent:639 - -Dio.netty.noJavassist: false
14:34:19,247 [main]DEBUG PlatformDependent:648 - Javassist: available
14:34:19,248 [main]DEBUG PlatformDependent:671 - -Dio.netty.tmpdir: C:\Users\MODHEL~1\AppData\Local\Temp (java.io.tmpdir)
14:34:19,248 [main]DEBUG PlatformDependent:752 - -Dio.netty.bitMode: 64 (sun.arch.data.model)
14:34:19,250 [main]DEBUG PlatformDependent:84 - -Dio.netty.noPreferDirect: false
14:34:19,272 [main]DEBUG NioEventLoop:88 - -Dio.netty.noKeySetOptimization: false
14:34:19,272 [main]DEBUG NioEventLoop:89 - -Dio.netty.selectorAutoRebuildThreshold: 512
14:34:19,348 [main]DEBUG ThreadLocalRandom:139 - -Dio.netty.initialSeedUniquifier: 0x550417c5510bd44f
14:34:19,359 [main]DEBUG ChannelOutboundBuffer:57 - -Dio.netty.threadLocalDirectBufferSize: 65536
14:34:19,380 [main]DEBUG ByteBufUtil:55 - -Dio.netty.allocator.type: unpooled
14:34:19,389 [main]DEBUG NetUtil:100 - Loopback interface: Software Loopback Interface 1
14:34:19,390 [main]DEBUG NetUtil:105 - Loopback address: /0:0:0:0:0:0:0:1 (primary)
14:34:19,390 [main]DEBUG NetUtil:108 - Loopback address: /127.0.0.1
14:34:22,466 [nioEventLoopGroup-3-1]DEBUG JavassistTypeParameterMatcherGenerator:78 - Generated: io.netty.util.internal.matchers.io.netty.handler.codec.http.FullHttpRequestMatcher
14:34:22,488 [nioEventLoopGroup-3-1]DEBUG JavassistTypeParameterMatcherGenerator:78 - Generated: io.netty.util.internal.matchers.io.netty.handler.codec.http.HttpObjectMatcher
14:34:22,493 [nioEventLoopGroup-3-1]DEBUG ResourceLeakDetector:91 - -Dio.netty.leakDetectionLevel: simple
14:34:22,501 [nioEventLoopGroup-3-1]DEBUG DefaultChannelPipeline:966 - Discarded inbound message DefaultFullHttpRequest(decodeResult: success)
GET / HTTP/1.1
Host: localhost:8082
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: __utma=111872281.1263245855.1382702526.1382704463.1387531722.3; __utmz=111872281.1382702526.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)
Connection: keep-alive
Cache-Control: max-age=0
Content-Length: 0 that reached at the tail of the pipeline. Please check your pipeline configuration.

@normanmaurer
Member

@twilek sorry ... this totally slipped. Is it still happen with latest release and if so could you provide a reproducer ?

@maekitalo

I have got the same error message in my code. I try to implement a http client with keep alive. Here is my full example:

package de.exchange.maektom.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.CharsetUtil;

import java.net.URI;
import java.util.concurrent.TimeUnit;

public final class HttpClient {

    private URI uri;
    private HttpRequest request;
    private EventLoopGroup group;
    private Bootstrap bootstrap;
    private Channel channel;
    private ReplyCallback reply;
    private ChannelFutureListener connectListener = new ChannelFutureListener() {

        @Override
        public void operationComplete(ChannelFuture future)
                throws Exception {

            if (!future.isSuccess()) {
                reply.onFail(future.cause());
            }

        }
    };

    private Bootstrap getBootstrap() {

        if (bootstrap == null) {

            bootstrap = new Bootstrap();
            bootstrap.group(group)
             .channel(NioSocketChannel.class)
             .remoteAddress(uri.getHost(), uri.getPort())
             .handler(new ChannelInitializer<SocketChannel> () {

                @Override
                public void initChannel(SocketChannel ch) {
                    final ChannelPipeline p = ch.pipeline();

                    p.addLast(new HttpClientCodec());
                    p.addLast(new HttpContentDecompressor());
                    p.addLast(new SimpleChannelInboundHandler<HttpContent>() {

                        @Override
                        public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause) throws Exception {

                            reply.onFail(cause);
                        }

                        @Override
                        public void channelActive(ChannelHandlerContext ctx)
                                throws Exception {

                            channel = ctx.channel();

                            if (request != null)
                                channel.writeAndFlush(request);
                        }

                        @Override
                        public void channelInactive(ChannelHandlerContext ctx)
                                throws Exception {

                            bootstrap = null;
                            channel = null;

                            if (request != null)
                                getBootstrap().connect().addListener(connectListener);
                        }

                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx,
                                HttpContent content) throws Exception {

                            request = null;                         
                            reply.onReply(content);
                        }

                    });
                }
             });
        }

        return bootstrap;
    }

    public interface ReplyCallback {
        void onReply(HttpContent content);
        void onFail(Throwable cause);
    }

    public HttpClient(EventLoopGroup group, URI uri, ReplyCallback reply) {
        this.group = group;
        this.reply = reply;
        this.uri = uri;
    }

    public void executeRequest(HttpRequest request, ReplyCallback reply) throws Exception {

        this.request = request;

        if (channel == null) {

            // case first request, no connection yet
            System.out.println("call connect");
            getBootstrap().connect().addListener(connectListener);
        }
        else {

            // case keep alive request
            System.out.println("send request");
            channel.writeAndFlush(request);
        }
    }

    public static void main(String[] args) {

        final EventLoopGroup group = new NioEventLoopGroup();

        try {

            final HttpRequest request = new DefaultFullHttpRequest(
                    HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
            request.headers().set(HttpHeaders.Names.HOST, "localhost");
            request.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
            request.headers().set(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP);

            final ReplyCallback cb = new ReplyCallback() {

                @Override
                public void onReply(HttpContent content) {

                    System.out.print(content.content().toString(CharsetUtil.UTF_8));
                    System.out.flush();
                }

                @Override
                public void onFail(Throwable cause) {
                    cause.printStackTrace();

                }
            };

            final HttpClient cli = new HttpClient(group, new URI("http://localhost:8000/"), cb);

            group.scheduleAtFixedRate(new Runnable() {

                @Override
                public void run() {
                    System.out.println("execute request");
                    try {
                        cli.executeRequest(request, cb);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            }, 1, 5, TimeUnit.SECONDS);

            Thread.sleep(60000);

        } catch (Exception e) {
            e.printStackTrace();

        } finally {

            group.shutdownGracefully();
        }
    }
}

I get:

Sep 12, 2014 2:38:28 PM io.netty.channel.ChannelOutboundBuffer safeSuccess
WARNUNG: Failed to mark a promise as success because it is done already: DefaultChannelPromise@108137c9(failure(io.netty.handler.codec.EncoderException: io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1)

It happens with netty-4.0.23.Final and netty-4.1.0.Beta3. It may well be a misunderstanding in my code since I am very new to netty.

@lexand
lexand commented Oct 21, 2014

Salutations

I have same issue on netty-4.0.23.Final

below is my investigations that may helps someone
and shed some light on the problem

source code before modifications
    // ........
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast(LOG_HANDLER);

        pipeline.addLast(new HttpResponseEncoder());
        pipeline.addLast(new HttpRequestDecoder());
        pipeline.addLast(new HttpObjectAggregator(MAX_CONTENT_LENGTH));

        pipeline.addLast(baseHandler);
    }

    // ........
    public static void writeResponse(ChannelHandlerContext ctx, HttpResponse response) {
        if (!response.headers().contains(HttpHeaders.Names.CONTENT_LENGTH)) {
            response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, 0);
        }

        ChannelFuture future = ctx.writeAndFlush(response);
        //this future has the same object hash as in error 53343fce
        //Failed to mark a promise as success because it is done already: DefaultChannelPromise@53343fce(failure(io.netty.handler.codec.EncoderException: io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1)

        // Close the non-keep-alive connection after the write operation is done.
        future.addListener(ChannelFutureListener.CLOSE);
    }

and this is the place where writeResponse() called

    private static final ByteBuf ACCEPTED = Unpooled.copiedBuffer(
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?> <report><status errorCode=\"0\">Accepted</status></report>",
            CharsetUtil.UTF_8
                                                                 );

    public void sendOkResponse(ChannelHandlerContext ctx) {
        try {
            DefaultFullHttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.OK,
                    ACCEPTED,
                    false
            );
            response.headers().add(HttpHeaders.Names.CONTENT_TYPE, "application/xml");
            response.headers().add(HttpHeaders.Names.CONTENT_LENGTH, ACCEPTED.capacity());
            HttpHelper.writeResponse(ctx, response);
        }
        catch (Exception e) {
            throw new DlrProcessingException(e);
        }
    }
the solution for me is
    // ***************************************
    // makes this buffer unreleasable
    // ***************************************
    private static final ByteBuf ACCEPTED = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?> <report><status errorCode=\"0\">Accepted</status></report>",
            CharsetUtil.UTF_8
                                                                 ));

    public void sendOkResponse(ChannelHandlerContext ctx) {
        try {
            // ***************************************
            // reset reader index
            // ***************************************
            ACCEPTED.resetReaderIndex();

            DefaultFullHttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.OK,
                    ACCEPTED,
                    false
            );
            response.headers().add(HttpHeaders.Names.CONTENT_TYPE, "application/xml");
            response.headers().add(HttpHeaders.Names.CONTENT_LENGTH, ACCEPTED.capacity());
            HttpHelper.writeResponse(ctx, response);
        }
        catch (Exception e) {
            throw new DlrProcessingException(e);
        }
    }
@trustin trustin modified the milestone: 4.0.26.Final, 4.0.25.Final Dec 31, 2014
@chemist777

In latest Netty 5 i had similar exceptions.

Cause was this code fragment:

if (frame instanceof CloseWebSocketFrame) {
    ctx.channel().write(frame.retain()).addListener(ChannelFutureListener.CLOSE);
    return;
}

After this changes (flush) exceptions disappeared:

if (frame instanceof CloseWebSocketFrame) {
    ctx.channel().writeAndFlush(frame.retain()).addListener(ChannelFutureListener.CLOSE);
    return;
}
@LexManos LexManos added a commit to MinecraftForge/MinecraftForge that referenced this issue May 27, 2015
@LexManos LexManos Force netty downgrade on dedicated server to match client. Netty bug: n… 2bb090b
@trustin
Member
trustin commented Aug 21, 2015

Bad taste? Bad taste.
bad taste to bad taste

@cpw
cpw commented Dec 13, 2015

milestone bump Nice..

@trustin
Member
trustin commented Dec 14, 2015

TL;DR - Closing, because it's a user error.

According to the original report from @twilek, marking the promise as success has failed because the promise has been failed already due to IllegalReferenceCountException. As the exception signifies, it means a user (or some component of Netty) attempted to decrease (or increase) the reference count of a buffer in an unexpected way.

What @lexand shared in his comment is one of the common user mistakes. He (or she) created a buffer as a static field and reused it over and over. When a buffer is written out to a socket, Netty releases the buffer (i.e. decreases its reference count.) As @lexand fixes his/her problem, you need to make such a buffer unreleasable or you have to call retain() on each write.

The reproducer that @maekitalo posted also has the same problem with what @lexand experienced. He or she created a DefaultFullHttpRequest and reused it without retaining it. Why DefaultFullHttpRequest? It's because a DefaultFullHttpRequest allocates its own content buffer unless you specified the content. If the content is empty, you could specify Unpooled.EMPTY_BUFFER explicitly. It's unreleasable.

I'm not sure if the problem @chemist777 posted is related with the original report. At least our WebSocket support code seems to be fixed now.

Other complaints about our milestone bumping..? Without more useful information, they are just begging to get marked as a 'user error'.

@trustin trustin closed this Dec 14, 2015
@trustin trustin locked and limited conversation to collaborators Dec 14, 2015
@trustin trustin removed this from the 4.0.34.Final milestone Dec 14, 2015
@trustin trustin added the not a bug label Dec 14, 2015
@trustin trustin self-assigned this Dec 14, 2015
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.